feat: add local autosave history prototype
This commit is contained in:
+223
-4
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user