Files
mnote/scripts/task-page-aggregate-options-sync-smoke.js

254 lines
11 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
cleanupDocuments,
createTempDocument,
ensureAuthenticated,
openDocument,
requestJson,
} = require("./tree-shell-smoke-helpers");
const OUT_DIR = path.join(process.cwd(), "tmp", "page-aggregate-options-sync-smoke");
async function saveDocumentContent(requestContext, workspaceId, documentId) {
await requestJson(requestContext, "/api/documents/save", {
method: "POST",
data: {
workspaceId,
documentId,
content: [
{
id: "h1",
type: "heading",
props: { level: 1 },
content: [{ type: "text", text: "Page Aggregate Options Smoke" }],
},
{
id: "p1",
type: "paragraph",
content: [{ type: "text", text: "Body for page aggregate options smoke." }],
},
],
blockCount: 2,
snapshotCapturedAt: new Date().toISOString(),
},
});
}
async function fetchPageAggregate(requestContext, workspaceId, documentId) {
const payload = await requestJson(
requestContext,
`/api/page-aggregate/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
{ method: "GET" },
);
assert.equal(payload.schema, "mnote.page_aggregate.v1", "Page Aggregate schema 应保持稳定");
return payload.result;
}
async function openPageSettingsDialog(page) {
const trigger = page.getByTestId("wolai-page-settings-trigger");
await trigger.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await trigger.click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-option-checkbox="wideLayout"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
}
async function readRuntimePageOptions(page) {
return page.evaluate(() => {
const shell = document.querySelector(".document-shell");
const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
const editorSurface = editorRoot?.querySelector(".editor-surface");
const editor = editorRoot?.querySelector(".ProseMirror");
const firstParagraph = editorRoot?.querySelector(".ProseMirror p");
return {
htmlWide: document.documentElement.getAttribute("data-page-wide-layout"),
htmlSmall: document.documentElement.getAttribute("data-page-small-text"),
htmlDensity: document.documentElement.getAttribute("data-layout-density"),
shellWide: shell instanceof HTMLElement ? shell.getAttribute("data-page-wide-layout") : null,
shellSmall: shell instanceof HTMLElement ? shell.getAttribute("data-page-small-text") : null,
shellDensity: shell instanceof HTMLElement ? shell.getAttribute("data-layout-density") : null,
shellMaxWidth:
shell instanceof HTMLElement ? shell.style.maxWidth || window.getComputedStyle(shell).maxWidth : null,
editorRootWide: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-wide-layout") : null,
editorRootSmall: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-small-text") : null,
editorSurfaceDensity:
editorSurface instanceof HTMLElement ? editorSurface.getAttribute("data-layout-density") : null,
editorFontSize: editor instanceof HTMLElement ? window.getComputedStyle(editor).fontSize : null,
paragraphMarginBottom:
firstParagraph instanceof HTMLElement ? window.getComputedStyle(firstParagraph).marginBottom : null,
};
});
}
async function waitForOptionsResponse(page, documentId, mutate) {
const responsePromise = page.waitForResponse(
async (response) => {
if (!response.url().includes("/api/ui/preferences") || response.request().method() !== "PUT") {
return false;
}
const payload = response.request().postDataJSON();
return payload?.documentId === documentId && response.ok();
},
{ timeout: UI_TIMEOUT_MS },
);
await mutate();
const response = await responsePromise;
const payload = await response.json();
assert.equal(payload?.owner, "mnote-web", "页面设置写入必须由 mnote-web 持有");
assert(payload?.result?.pageOptions, "页面设置写入必须返回 SQLite 合并后的 pageOptions");
return payload;
}
async function waitForAggregateOption(requestContext, workspaceId, documentId, assertOptions) {
const deadline = Date.now() + UI_TIMEOUT_MS;
let aggregate = null;
while (Date.now() < deadline) {
aggregate = await fetchPageAggregate(requestContext, workspaceId, documentId);
const options = aggregate?.layout?.pageOptions ?? {};
if (assertOptions(options)) {
return { aggregate, options };
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
throw new Error(
`等待 Page Aggregate pageOptions 同步超时:${JSON.stringify({
documentId,
lastOptions: aggregate?.layout?.pageOptions ?? null,
})}`,
);
}
async function main() {
await fs.mkdir(OUT_DIR, { recursive: true });
const suffix = Date.now().toString(36);
const evidencePath = path.join(OUT_DIR, `${suffix}.json`);
const screenshotPath = path.join(OUT_DIR, `${suffix}.png`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
const page = await context.newPage();
const createdIds = [];
try {
const viewer = await ensureAuthenticated(page, context.request);
const target = await createTempDocument(context.request, null);
createdIds.push(target.documentId);
await saveDocumentContent(context.request, target.workspaceId, target.documentId);
await openDocument(page, target.workspaceId, target.documentId);
await page.locator(".ProseMirror").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await openPageSettingsDialog(page);
const initialAggregate = await fetchPageAggregate(context.request, target.workspaceId, target.documentId);
const initialRuntime = await readRuntimePageOptions(page);
assert.equal(initialAggregate.layout.pageOptions.wideLayout, false, "初始 wideLayout 应为 false");
assert.equal(initialAggregate.layout.pageOptions.smallText, false, "初始 smallText 应为 false");
assert.equal(initialAggregate.layout.pageOptions.layoutDensity, "normal", "初始 layoutDensity 应为 normal");
assert.equal(initialRuntime.htmlWide, "false", "初始 runtime wideLayout 应为 false");
assert.equal(initialRuntime.htmlSmall, "false", "初始 runtime smallText 应为 false");
assert.equal(initialRuntime.htmlDensity, "normal", "初始 runtime layoutDensity 应为 normal");
const wideResponse = await waitForOptionsResponse(page, target.documentId, async () => {
await page.locator('[data-page-option-checkbox="wideLayout"]').click({ timeout: UI_TIMEOUT_MS });
});
const wideAggregate = await waitForAggregateOption(
context.request,
target.workspaceId,
target.documentId,
(options) => options.wideLayout === true,
);
const afterWideRuntime = await readRuntimePageOptions(page);
assert.equal(afterWideRuntime.htmlWide, "true", "开启宽版后 html wideLayout 应为 true");
assert.equal(afterWideRuntime.editorRootWide, "true", "开启宽版后 island root wideLayout 应为 true");
assert.equal(afterWideRuntime.shellMaxWidth, "980px", "开启宽版后主列宽应为 980px");
const smallResponse = await waitForOptionsResponse(page, target.documentId, async () => {
await page.locator('[data-page-option-checkbox="smallText"]').click({ timeout: UI_TIMEOUT_MS });
});
const smallAggregate = await waitForAggregateOption(
context.request,
target.workspaceId,
target.documentId,
(options) => options.wideLayout === true && options.smallText === true,
);
const afterSmallRuntime = await readRuntimePageOptions(page);
assert.equal(afterSmallRuntime.htmlSmall, "true", "开启小字体后 html smallText 应为 true");
assert.equal(afterSmallRuntime.editorRootSmall, "true", "开启小字体后 island root smallText 应为 true");
assert(
typeof initialRuntime.editorFontSize === "string" &&
typeof afterSmallRuntime.editorFontSize === "string" &&
parseFloat(afterSmallRuntime.editorFontSize) < parseFloat(initialRuntime.editorFontSize),
`开启小字体后编辑器字号应变小,初始 ${initialRuntime.editorFontSize},实际 ${afterSmallRuntime.editorFontSize}`,
);
const densityResponse = await waitForOptionsResponse(page, target.documentId, async () => {
await page.locator('[data-page-settings-tab="custom"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-option-select="layoutDensity"]').selectOption("compact");
});
const densityAggregate = await waitForAggregateOption(
context.request,
target.workspaceId,
target.documentId,
(options) => options.wideLayout === true && options.smallText === true && options.layoutDensity === "compact",
);
const afterDensityRuntime = await readRuntimePageOptions(page);
assert.equal(afterDensityRuntime.htmlDensity, "compact", "切换紧凑后 html density 应为 compact");
assert.equal(afterDensityRuntime.editorSurfaceDensity, "compact", "切换紧凑后 island surface density 应为 compact");
assert(
typeof initialRuntime.paragraphMarginBottom === "string" &&
typeof afterDensityRuntime.paragraphMarginBottom === "string" &&
parseFloat(afterDensityRuntime.paragraphMarginBottom) <= parseFloat(initialRuntime.paragraphMarginBottom),
`切换紧凑后段落间距应不大于初始值,初始 ${initialRuntime.paragraphMarginBottom},实际 ${afterDensityRuntime.paragraphMarginBottom}`,
);
await page.screenshot({ path: screenshotPath, fullPage: false });
const evidence = {
ok: true,
baseUrl: BASE_URL,
viewerUserId: viewer.userId,
workspaceId: target.workspaceId,
documentId: target.documentId,
initial: {
aggregate: initialAggregate.layout.pageOptions,
runtime: initialRuntime,
},
afterWide: {
aggregate: wideAggregate.options,
runtime: afterWideRuntime,
commandName: wideResponse.meta.commandName,
},
afterSmall: {
aggregate: smallAggregate.options,
runtime: afterSmallRuntime,
commandName: smallResponse.meta.commandName,
},
afterDensity: {
aggregate: densityAggregate.options,
runtime: afterDensityRuntime,
commandName: densityResponse.meta.commandName,
},
screenshotPath,
evidencePath,
};
await fs.writeFile(evidencePath, JSON.stringify(evidence, null, 2), "utf8");
console.log(JSON.stringify(evidence, null, 2));
} finally {
await cleanupDocuments(context.request, createdIds).catch(() => undefined);
await page.close().catch(() => undefined);
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});