feat: add local autosave history prototype

This commit is contained in:
QiuSW
2026-08-04 23:00:05 +08:00
parent b62e9697a4
commit a82af6939f
31 changed files with 1730 additions and 73 deletions
+223 -4
View File
@@ -19,7 +19,17 @@ const cases = [
{ view: "static", file: "05-result-static.png", fullPage: true },
{ view: "consent", file: "06-ai-consent.png", fullPage: false },
{ view: "explanation", file: "07-ai-explanation.png", fullPage: true },
{ view: "error", file: "08-ai-error.png", fullPage: true }
{ view: "error", file: "08-ai-error.png", fullPage: true },
{ view: "home", file: "09-home-returning.png", fullPage: true },
{ view: "home-empty", file: "10-home-empty.png", fullPage: true },
{ view: "history", file: "11-history-list.png", fullPage: true },
{ view: "history-empty", file: "12-history-empty.png", fullPage: true },
{ view: "history-detail", file: "13-history-detail.png", fullPage: true },
{ view: "settings", file: "14-settings-default.png", fullPage: true },
{ view: "delete", file: "15-delete-confirm.png", fullPage: false },
{ view: "settings-off", file: "16-settings-off.png", fullPage: true },
{ view: "result-not-saved", file: "17-result-not-saved.png", fullPage: true },
{ view: "clear-all", file: "18-clear-all-confirm.png", fullPage: false }
];
function findChrome() {
@@ -206,6 +216,26 @@ async function auditView(client, view) {
})()`);
}
async function auditLayout(client, { name, width, height, view }) {
await client.send("Emulation.setDeviceMetricsOverride", {
width,
height,
deviceScaleFactor: 1,
mobile: false,
screenWidth: width,
screenHeight: height
});
await navigate(client, view);
return evaluate(client, `(() => ({
name: ${JSON.stringify(name)},
view: ${JSON.stringify(view)},
viewport: { width: innerWidth, height: innerHeight },
documentWidth: document.documentElement.scrollWidth,
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth,
mainReachable: Boolean(document.querySelector('#screen'))
}))()`);
}
async function run() {
const health = await fetch(baseUrl);
if (!health.ok) {
@@ -265,6 +295,18 @@ async function run() {
const screenshots = [];
for (const item of cases) screenshots.push(await capture(client, item));
await navigate(client, "casting");
for (let remainingRound = 0; remainingRound < 4; remainingRound += 1) {
for (let coinIndex = 0; coinIndex < 3; coinIndex += 1) {
await evaluate(client, `document.querySelector('[data-action="toggle-coin"][data-index="${coinIndex}"]')?.click()`);
}
await evaluate(client, "document.querySelector('[data-action=\"confirm-line\"]')?.click()");
}
const completedCastingSave = await evaluate(client, `(() => ({
resultVisible: Boolean(document.querySelector('#result-title')),
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim()
}))()`);
await navigate(client, "result");
const movingResult = await evaluate(client, `(() => ({
names: [...document.querySelectorAll(".hexagram-name")].map((node) => node.textContent.trim()),
@@ -289,28 +331,158 @@ async function run() {
await evaluate(client, "document.querySelector('[data-action=\"confirm-ai\"]')?.click()");
await delay(1050);
const explanationSource = await evaluate(client, "document.querySelector('.source-badge')?.textContent.trim()");
const explanationSaveStatus = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
await navigate(client, "home");
const homeState = await evaluate(client, `(() => ({
primaryActions: document.querySelectorAll('[data-action="begin"]').length,
recentRoute: document.querySelector('.recent-card__route')?.textContent.replace(/\\s+/g, " ").trim(),
historyEntry: document.querySelectorAll('[data-action="open-history"]').length,
settingsEntry: document.querySelectorAll('[data-action="open-settings"]').length,
localNotice: document.querySelector('[data-local-data-notice]')?.textContent.replace(/\\s+/g, " ").trim(),
bottomNavigation: document.querySelectorAll('nav, [role="navigation"]').length
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"begin\"]')?.click()");
const homeForwardTitle = await evaluate(client, "document.querySelector('#question-title')?.textContent.trim()");
await evaluate(client, "document.querySelector('[data-action=\"back\"]')?.click()");
const homeBackTitle = await evaluate(client, "document.querySelector('#home-title')?.textContent.trim()");
await navigate(client, "history");
const historyState = await evaluate(client, `(() => ({
count: document.querySelectorAll('.history-card').length,
firstRoute: document.querySelector('.history-card__route')?.textContent.replace(/\\s+/g, " ").trim()
}))()`);
await navigate(client, "history-empty");
const historyEmptyState = await evaluate(client, `(() => ({
hasEmptyGuidance: Boolean(document.querySelector('[data-history-empty]')),
hasPrimaryAction: Boolean(document.querySelector('[data-action="begin"]')),
hasSettingsEntry: Boolean(document.querySelector('[data-action="open-settings"]'))
}))()`);
await navigate(client, "settings");
const settingsDefaults = await evaluate(client, `(() => ({
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
question: document.querySelector('[data-action="save-question-setting"]')?.checked,
explanation: document.querySelector('[data-action="save-explanation-setting"]')?.checked,
action: document.querySelector('[data-action="save-action-setting"]')?.checked,
detailDisabled: [...document.querySelectorAll('.settings-card__details input')].some((input) => input.disabled),
boundary: document.querySelector('.settings-boundary')?.textContent.replace(/\\s+/g, " ").trim()
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"auto-save-toggle\"]')?.click()");
await delay(100);
const settingsDisabled = await evaluate(client, `(() => ({
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
detailsDisabled: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.disabled),
retainedValues: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.checked)
}))()`);
await navigate(client, "result");
const autoSaveBefore = await evaluate(client, `(() => ({
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
canOptOut: Boolean(document.querySelector('[data-action="remove-current-record"]'))
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"remove-current-record\"]')?.click()");
const optOutDialogVisible = await evaluate(client, "Boolean(document.querySelector('[role=\"alertdialog\"]'))");
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
await delay(100);
const afterOptOut = await evaluate(client, `(() => ({
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"save-current\"]')?.click()");
await delay(100);
const afterManualSave = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
await navigate(client, "result-not-saved");
const autoSaveOffResult = await evaluate(client, `(() => ({
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
}))()`);
await navigate(client, "clear-all");
const clearBefore = await evaluate(client, `(() => ({
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
countText: document.querySelector('#clear-modal-description')?.textContent.trim()
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"confirm-clear-all\"]')?.click()");
await delay(100);
const clearAfter = await evaluate(client, `(() => ({
settingsVisible: document.querySelector('#settings-title')?.textContent.trim(),
recordCount: document.querySelector('.settings-danger p')?.textContent.trim(),
clearDisabled: document.querySelector('[data-action="open-clear-all"]')?.disabled,
autoSaveRetained: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
modalClosed: !document.querySelector('[role="alertdialog"]')
}))()`);
await navigate(client, "delete");
const deleteBefore = await evaluate(client, `(() => ({
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
detailVisible: Boolean(document.querySelector('#history-detail-title'))
}))()`);
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
await delay(100);
const deleteAfter = await evaluate(client, `(() => ({
onHistory: document.querySelector('#history-title')?.textContent.trim(),
historyCount: document.querySelectorAll('.history-card').length,
modalClosed: !document.querySelector('[role="alertdialog"]')
}))()`);
const accessibility = [];
for (const item of cases) accessibility.push(await auditView(client, item.view));
const layoutAudits = [];
for (const layout of [
{ name: "small-phone", width: 375, height: 812, view: "home" },
{ name: "large-phone", width: 430, height: 932, view: "history" },
{ name: "large-phone-settings", width: 430, height: 932, view: "settings" },
{ name: "phone-landscape", width: 792, height: 360, view: "history-detail" }
]) {
layoutAudits.push(await auditLayout(client, layout));
}
await client.send("Emulation.setEmulatedMedia", {
features: [{ name: "prefers-reduced-motion", value: "reduce" }]
});
await navigate(client, "home");
const reducedMotion = await evaluate(client, `(() => {
const page = document.querySelector('.page');
const duration = Number.parseFloat(getComputedStyle(page).animationDuration) || 0;
return { animationDurationSeconds: duration, reduced: duration <= 0.001 };
})()`);
await client.send("Emulation.setEmulatedMedia", { features: [] });
const report = {
chrome,
baseUrl,
screenshots,
completedCastingSave,
movingResult,
staticResult,
consentGate: {
disabledBeforeConsent: consentBefore,
disabledAfterConsent: consentAfter,
explanationSource
explanationSource,
explanationSaveStatus
},
homeState: { ...homeState, forwardTitle: homeForwardTitle, backTitle: homeBackTitle },
historyState,
historyEmptyState,
settingsFlow: { defaults: settingsDefaults, disabled: settingsDisabled },
autoSaveFlow: { before: autoSaveBefore, optOutDialogVisible, afterOptOut, afterManualSave, autoSaveOffResult },
clearAllFlow: { before: clearBefore, after: clearAfter },
deleteFlow: { before: deleteBefore, after: deleteAfter },
consoleErrors,
runtimeErrors,
externalRequests: [...new Set(externalRequests)],
accessibility
accessibility,
layoutAudits,
reducedMotion
};
const failed =
completedCastingSave.resultVisible !== true ||
!completedCastingSave.status?.includes("已自动保存到本机") ||
movingResult.names.join(",") !== "复,坤" ||
movingResult.numbers.join(",") !== "第 24 卦,第 2 卦" ||
!movingResult.moving.includes("1 个动爻") ||
@@ -319,12 +491,59 @@ async function run() {
consentBefore !== true ||
consentAfter !== false ||
explanationSource !== "AI 生成" ||
!explanationSaveStatus?.includes("解读已加入本次记录") ||
homeState.primaryActions !== 1 ||
homeState.recentRoute !== "复 之 坤" ||
homeState.historyEntry < 1 ||
homeState.settingsEntry !== 1 ||
!homeState.localNotice?.includes("起卦与历史默认保存在本机,不主动上传") ||
!homeState.localNotice?.includes("只有你选择 AI 解读时,本次所需内容才会发送") ||
homeState.bottomNavigation !== 0 ||
homeForwardTitle !== "你想看清什么?" ||
homeBackTitle !== "此刻,有什么想安静看清?" ||
historyState.count !== 3 ||
historyState.firstRoute !== "复 之 坤" ||
historyEmptyState.hasEmptyGuidance !== true ||
historyEmptyState.hasPrimaryAction !== true ||
historyEmptyState.hasSettingsEntry !== true ||
settingsDefaults.autoSave !== true ||
settingsDefaults.question !== true ||
settingsDefaults.explanation !== true ||
settingsDefaults.action !== true ||
settingsDefaults.detailDisabled !== false ||
!settingsDefaults.boundary?.includes("默认排除系统备份、设备迁移与云同步") ||
settingsDisabled.autoSave !== false ||
settingsDisabled.detailsDisabled !== true ||
settingsDisabled.retainedValues !== true ||
!autoSaveBefore.status?.includes("已自动保存到本机") ||
autoSaveBefore.canOptOut !== true ||
optOutDialogVisible !== true ||
!afterOptOut.status?.includes("本次未保存") ||
!afterOptOut.status?.includes("你已选择不保留这次记录") ||
afterOptOut.canSave !== true ||
!afterManualSave?.includes("已保存到本机问卦簿") ||
!autoSaveOffResult.status?.includes("自动保存已关闭") ||
autoSaveOffResult.canSave !== true ||
clearBefore.dialogVisible !== true ||
!clearBefore.countText?.includes("当前 3 条记录") ||
clearAfter.settingsVisible !== "保存设置" ||
!clearAfter.recordCount?.includes("0 条记录") ||
clearAfter.clearDisabled !== true ||
clearAfter.autoSaveRetained !== true ||
clearAfter.modalClosed !== true ||
deleteBefore.dialogVisible !== true ||
deleteBefore.detailVisible !== true ||
deleteAfter.onHistory !== "问卦簿" ||
deleteAfter.historyCount !== 2 ||
deleteAfter.modalClosed !== true ||
consoleErrors.length > 0 ||
runtimeErrors.length > 0 ||
externalRequests.length > 0 ||
accessibility.some((item) =>
item.unnamedButtons > 0 || item.touchIssues.length > 0 || item.smallCheckboxLabels > 0
);
) ||
layoutAudits.some((item) => item.horizontalOverflow || !item.mainReachable) ||
reducedMotion.reduced !== true;
console.log(JSON.stringify(report, null, 2));
if (failed) process.exitCode = 1;