254 lines
11 KiB
JavaScript
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);
|
|
});
|