293 lines
12 KiB
Python
293 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Targeted, read-only DOM assertions for the archive/AI HTML shell."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
os.environ.setdefault("QT_SCALE_FACTOR", "1")
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.argv = [sys.argv[0], "--qt-smoke-test"]
|
|
|
|
from PySide6.QtCore import QSize, QTimer
|
|
from PySide6.QtWidgets import QApplication, QInputDialog
|
|
|
|
from wechat_gui_qt import MainWindow
|
|
|
|
|
|
app = QApplication.instance() or QApplication(sys.argv)
|
|
window = MainWindow()
|
|
window.setFixedSize(QSize(1630, 920))
|
|
window.show()
|
|
|
|
results = {}
|
|
|
|
|
|
def evaluate(name: str, page_index: int, script: str, done) -> None:
|
|
window.show_page(page_index)
|
|
|
|
def run() -> None:
|
|
window.console_shell.page().runJavaScript(script, finish)
|
|
|
|
def finish(value) -> None:
|
|
if isinstance(value, str) and value:
|
|
try:
|
|
value = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
results[name] = value
|
|
done()
|
|
|
|
QTimer.singleShot(650, run)
|
|
|
|
|
|
COMMON = r"""
|
|
const rect = selector => {
|
|
const el = document.querySelector(selector);
|
|
if (!el) return null;
|
|
const box = el.getBoundingClientRect();
|
|
return {x:box.x,y:box.y,width:box.width,height:box.height};
|
|
};
|
|
"""
|
|
|
|
ARCHIVE = """JSON.stringify((() => { try {\n""" + COMMON + r"""
|
|
return {
|
|
ok: true,
|
|
view: STATE.view,
|
|
title: document.querySelector('.page-head h1')?.textContent,
|
|
filter: rect('.filter-row'),
|
|
grid: rect('.archive-grid'),
|
|
gridColumns: getComputedStyle(document.querySelector('.archive-grid')).gridTemplateColumns,
|
|
bottom: rect('.archive-bottom'),
|
|
sessionRows: document.querySelectorAll('[data-action="select-archive"]').length,
|
|
filters: document.querySelectorAll('[data-action="archive-filter"]').length,
|
|
searchMaxLength: document.querySelector('#archive-search')?.maxLength,
|
|
actions: [...document.querySelectorAll('[data-action]')].map(x=>x.dataset.action),
|
|
toastShown: document.querySelector('#toast')?.classList.contains('show') || false
|
|
};
|
|
} catch (error) { return {ok:false,error:String(error),stack:error.stack}; } })())"""
|
|
|
|
AI = """JSON.stringify((() => { try {\n""" + COMMON + r"""
|
|
return {
|
|
ok: true,
|
|
view: STATE.view,
|
|
title: document.querySelector('.page-head h1')?.textContent,
|
|
tabs: rect('.tabs'),
|
|
top: rect('.ai-top'),
|
|
topColumns: getComputedStyle(document.querySelector('.ai-top')).gridTemplateColumns,
|
|
bottom: rect('.ai-bottom'),
|
|
bottomColumns: getComputedStyle(document.querySelector('.ai-bottom')).gridTemplateColumns,
|
|
config: rect('.config-strip'),
|
|
hiddenPanes: document.querySelectorAll('.ai-pane[hidden]').length,
|
|
promptReadOnly: document.querySelector('#ai-prompt')?.readOnly === true,
|
|
toolSwitches: document.querySelectorAll('[data-action="toggle-tool"]').length,
|
|
tabsWithActions: document.querySelectorAll('[data-action="ai-tab"]').length,
|
|
actions: [...document.querySelectorAll('[data-action]')].map(x=>x.dataset.action),
|
|
toastShown: document.querySelector('#toast')?.classList.contains('show') || false
|
|
};
|
|
} catch (error) { return {ok:false,error:String(error),stack:error.stack}; } })())"""
|
|
|
|
RUNTIME_ARCHIVE = """JSON.stringify((() => { try {\n""" + COMMON + r"""
|
|
STATE.demo = false;
|
|
Object.assign(STATE, {
|
|
view: 'archive', archiveFilter: '全部', archiveQuery: '', archiveDate: '',
|
|
selectedArchive: 'archive-1',
|
|
archives: [{key:'archive-1',name:'真实客户',avatar:'真',preview:'真实归档摘要',time:'16:31',badge:'AI 已回复',count:2}],
|
|
chat: [{role:'user',time:'16:31:01',text:'真实消息'},{role:'assistant',time:'16:31:02',text:'真实回复'}],
|
|
memory: {tags:['真实标签'],text:'真实客户记忆',rounds:2},
|
|
settle: [{title:'挂号登记',hint:'真实记录',value:'1 次'}],
|
|
activities: []
|
|
});
|
|
render('archive');
|
|
const oldBridge = window.qtBridge;
|
|
const calls = [];
|
|
const stub = {
|
|
exportArchive:()=>calls.push('exportArchive'), setArchiveFilter:key=>calls.push('setArchiveFilter:'+key),
|
|
chooseArchiveDate:()=>calls.push('chooseArchiveDate'), selectArchive:key=>calls.push('selectArchive:'+key),
|
|
refresh:()=>calls.push('refresh'), copyArchive:()=>calls.push('copyArchive')
|
|
};
|
|
window.qtBridge = stub;
|
|
const stubbed = window.qtBridge === stub;
|
|
if (stubbed) {
|
|
['export-archive','archive-filter','archive-date','select-archive','refresh','copy-archive']
|
|
.forEach(action => document.querySelector(`[data-action="${action}"]`)?.click());
|
|
}
|
|
window.qtBridge = oldBridge;
|
|
return {
|
|
ok:true, title:document.querySelector('.page-head h1')?.textContent,
|
|
filter:rect('.filter-row'), grid:rect('.archive-grid'), bottom:rect('.archive-bottom'),
|
|
gridColumns:getComputedStyle(document.querySelector('.archive-grid')).gridTemplateColumns,
|
|
searchMaxLength:document.querySelector('#archive-search')?.maxLength,
|
|
toastActions:document.querySelectorAll('[data-action="toast"]').length,
|
|
stubbed, calls
|
|
};
|
|
} catch (error) { return {ok:false,error:String(error),stack:error.stack}; } })())"""
|
|
|
|
RUNTIME_AI = """JSON.stringify((() => { try {\n""" + COMMON + r"""
|
|
STATE.demo = false;
|
|
STATE.aiOk = true;
|
|
STATE.aiTab = '基础设置';
|
|
STATE.ai = {
|
|
name:'真实客服', prompt:'由安全模板生成的只读提示词', model:'real-model', backup:'云端策略',
|
|
context:8, maxTokens:500, temperature:'0.35', replyLengthIndex:1, mcpRounds:5,
|
|
knowledgeCount:'2', knowledgeConnected:true, syncLabel:'刚刚同步', tools:[true,false,true],
|
|
toolCount:2, synced:true
|
|
};
|
|
render('ai');
|
|
const oldBridge = window.qtBridge;
|
|
const calls = [];
|
|
const stub = {
|
|
saveAi:payload=>calls.push('saveAi:'+payload), testAi:()=>calls.push('testAi'),
|
|
pickModel:()=>calls.push('pickModel'), pickBackupModel:()=>calls.push('pickBackupModel'),
|
|
syncKnowledge:()=>calls.push('syncKnowledge'), editKnowledge:()=>calls.push('editKnowledge'),
|
|
toggleTool:key=>calls.push('toggleTool:'+key), setAiTab:key=>calls.push('setAiTab:'+key)
|
|
};
|
|
window.qtBridge = stub;
|
|
const stubbed = window.qtBridge === stub;
|
|
if (stubbed) {
|
|
['save-ai','test-ai','pick-model','pick-backup','sync-knowledge','edit-knowledge','toggle-tool','ai-tab']
|
|
.forEach(action => document.querySelector(`[data-action="${action}"]`)?.click());
|
|
}
|
|
window.qtBridge = oldBridge;
|
|
let savedPayload = {};
|
|
const savedCall = calls.find(value=>value.startsWith('saveAi:')) || '';
|
|
try { savedPayload = JSON.parse(savedCall.slice(7) || '{}'); } catch (_error) {}
|
|
return {
|
|
ok:true, title:document.querySelector('.page-head h1')?.textContent,
|
|
tabs:rect('.tabs'), top:rect('.ai-top'), bottom:rect('.ai-bottom'), config:rect('.config-strip'),
|
|
topColumns:getComputedStyle(document.querySelector('.ai-top')).gridTemplateColumns,
|
|
bottomColumns:getComputedStyle(document.querySelector('.ai-bottom')).gridTemplateColumns,
|
|
hiddenPanes:document.querySelectorAll('.ai-pane[hidden]').length,
|
|
promptReadOnly:document.querySelector('#ai-prompt')?.readOnly === true,
|
|
toolSwitches:document.querySelectorAll('[data-action="toggle-tool"]').length,
|
|
toastActions:document.querySelectorAll('[data-action="toast"]').length,
|
|
stubbed, calls, savedPayload, savedPayloadHasPrompt:Object.hasOwn(savedPayload,'prompt')
|
|
};
|
|
} catch (error) { return {ok:false,error:String(error),stack:error.stack}; } })())"""
|
|
|
|
|
|
def finish() -> None:
|
|
listing = window.business_page.recent_list
|
|
first_item = listing.item(0) if listing.count() else None
|
|
first_key = str(first_item.data(0x0100) or "") if first_item else ""
|
|
results["archive_selection"] = {
|
|
"existing": bool(first_key and window._select_archive_session(first_key)),
|
|
"stale": window._select_archive_session("__missing_archive__"),
|
|
}
|
|
with patch.object(QInputDialog, "getText", return_value=("2026-08-18", True)):
|
|
with patch.object(window.business_page, "_set_archive_date") as set_date:
|
|
window.console_shell.bridge.chooseArchiveDate()
|
|
results["archive_date"] = {
|
|
"called": set_date.call_count == 1,
|
|
"value": set_date.call_args.args[0] if set_date.call_args else "",
|
|
}
|
|
print(json.dumps(results, ensure_ascii=False, indent=2), flush=True)
|
|
archive = results.get("archive") or {}
|
|
ai = results.get("ai") or {}
|
|
expected_archive = (
|
|
archive.get("ok")
|
|
and archive.get("title") == "会话归档"
|
|
and archive.get("gridColumns") == "333px 605px 437px"
|
|
and round((archive.get("filter") or {}).get("height", 0)) == 64
|
|
and round((archive.get("grid") or {}).get("height", 0)) == 521
|
|
and round((archive.get("bottom") or {}).get("height", 0)) == 153
|
|
and archive.get("sessionRows") == 5
|
|
and archive.get("filters") == 4
|
|
and archive.get("searchMaxLength") == 80
|
|
and not archive.get("toastShown")
|
|
and results["archive_selection"]["existing"]
|
|
and not results["archive_selection"]["stale"]
|
|
and results["archive_date"] == {"called": True, "value": "2026-08-18"}
|
|
)
|
|
expected_ai = (
|
|
ai.get("ok")
|
|
and ai.get("title") == "AI 设置"
|
|
and ai.get("topColumns") == "856px 536px"
|
|
and ai.get("bottomColumns") == "428px 428px 536px"
|
|
and round((ai.get("tabs") or {}).get("height", 0)) == 58
|
|
and round((ai.get("top") or {}).get("height", 0)) == 330
|
|
and round((ai.get("bottom") or {}).get("height", 0)) == 244
|
|
and round((ai.get("config") or {}).get("height", 0)) == 110
|
|
and ai.get("hiddenPanes") == 0
|
|
and ai.get("promptReadOnly")
|
|
and ai.get("toolSwitches") == 3
|
|
and ai.get("tabsWithActions") == 3
|
|
and not ai.get("toastShown")
|
|
)
|
|
runtime_archive = results.get("runtime_archive") or {}
|
|
expected_runtime_archive = (
|
|
runtime_archive.get("ok")
|
|
and runtime_archive.get("title") == "会话归档"
|
|
and runtime_archive.get("gridColumns") == "333px 605px 437px"
|
|
and round((runtime_archive.get("filter") or {}).get("height", 0)) == 64
|
|
and round((runtime_archive.get("grid") or {}).get("height", 0)) == 521
|
|
and round((runtime_archive.get("bottom") or {}).get("height", 0)) == 153
|
|
and runtime_archive.get("searchMaxLength") == 80
|
|
and runtime_archive.get("toastActions") == 0
|
|
and runtime_archive.get("stubbed")
|
|
and runtime_archive.get("calls") == [
|
|
"exportArchive", "setArchiveFilter:全部会话", "chooseArchiveDate",
|
|
"selectArchive:archive-1", "refresh", "copyArchive",
|
|
]
|
|
)
|
|
runtime_ai = results.get("runtime_ai") or {}
|
|
expected_runtime_ai = (
|
|
runtime_ai.get("ok")
|
|
and runtime_ai.get("title") == "AI 设置"
|
|
and runtime_ai.get("topColumns") == "856px 536px"
|
|
and runtime_ai.get("bottomColumns") == "428px 428px 536px"
|
|
and round((runtime_ai.get("tabs") or {}).get("height", 0)) == 58
|
|
and round((runtime_ai.get("top") or {}).get("height", 0)) == 330
|
|
and round((runtime_ai.get("bottom") or {}).get("height", 0)) == 244
|
|
and round((runtime_ai.get("config") or {}).get("height", 0)) == 110
|
|
and runtime_ai.get("hiddenPanes") == 0
|
|
and runtime_ai.get("promptReadOnly")
|
|
and runtime_ai.get("toolSwitches") == 3
|
|
and runtime_ai.get("toastActions") == 0
|
|
and runtime_ai.get("stubbed")
|
|
and not runtime_ai.get("savedPayloadHasPrompt")
|
|
and (runtime_ai.get("savedPayload") or {}).get("maxTokens") == 500
|
|
and runtime_ai.get("calls") == [
|
|
"saveAi:{\"name\":\"真实客服\",\"temperature\":0.35,\"context\":8,\"maxTokens\":500,\"mcpRounds\":5,\"replyLength\":1}",
|
|
"testAi", "pickModel", "pickBackupModel", "syncKnowledge", "editKnowledge",
|
|
"toggleTool:0", "setAiTab:基础设置",
|
|
]
|
|
)
|
|
app.exit(
|
|
0
|
|
if expected_archive and expected_ai and expected_runtime_archive and expected_runtime_ai
|
|
else 1
|
|
)
|
|
|
|
|
|
def run_runtime_ai() -> None:
|
|
evaluate("runtime_ai", 3, RUNTIME_AI, finish)
|
|
|
|
|
|
def run_runtime_archive() -> None:
|
|
evaluate("runtime_archive", 2, RUNTIME_ARCHIVE, run_runtime_ai)
|
|
|
|
|
|
def run_ai() -> None:
|
|
evaluate("ai", 3, AI, run_runtime_archive)
|
|
|
|
|
|
def run_archive() -> None:
|
|
evaluate("archive", 2, ARCHIVE, run_ai)
|
|
|
|
|
|
def wait_until_ready() -> None:
|
|
if window.console_shell.page_ready:
|
|
run_archive()
|
|
else:
|
|
QTimer.singleShot(100, wait_until_ready)
|
|
|
|
|
|
QTimer.singleShot(100, wait_until_ready)
|
|
QTimer.singleShot(20000, lambda: app.exit(2))
|
|
raise SystemExit(app.exec())
|