新增功能
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
# -*- 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())
|
||||
Reference in New Issue
Block a user