Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
516 lines
21 KiB
JavaScript
516 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
/**
|
|
* Regression: page-tree expand/collapse icons + persistence + no duplicate rows.
|
|
* Builds a liaibo-like nested local_folder tree, exercises expand/collapse/reload.
|
|
*/
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const http = require("node:http");
|
|
const net = require("node:net");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { spawn } = require("node:child_process");
|
|
const { chromium } = require("playwright");
|
|
const { buildControlPlaneTestEnv } = require("./lib/control-plane-test-env");
|
|
|
|
const TASK = "task-page-tree-expand-persist-regression";
|
|
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 60_000);
|
|
const TEST_PASSWORD = process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join("");
|
|
const RESULT_DIR = path.join(__dirname, "..", "tmp", TASK);
|
|
const BINARY =
|
|
process.env.MNOTE_WEB_BIN ||
|
|
path.join(__dirname, "..", "rust", "target", "debug", "mnote-web");
|
|
|
|
function resolveChromiumExecutablePath() {
|
|
const explicit = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || "";
|
|
if (explicit && fs.existsSync(explicit)) return explicit;
|
|
return [
|
|
"/usr/bin/google-chrome-stable",
|
|
"/usr/bin/google-chrome",
|
|
"/snap/bin/chromium",
|
|
"/usr/bin/chromium",
|
|
"/usr/bin/chromium-browser",
|
|
].find((candidate) => fs.existsSync(candidate)) || "";
|
|
}
|
|
|
|
function pickPort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const address = server.address();
|
|
const port = address && typeof address === "object" ? address.port : 0;
|
|
server.close(() => resolve(port));
|
|
});
|
|
server.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function waitForHttpOk(url, timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
return new Promise((resolve, reject) => {
|
|
const tick = () => {
|
|
if (Date.now() > deadline) {
|
|
reject(new Error(`server_not_ready: ${url}`));
|
|
return;
|
|
}
|
|
const request = http.get(url, (response) => {
|
|
response.resume();
|
|
if (response.statusCode >= 200 && response.statusCode < 500) {
|
|
resolve();
|
|
return;
|
|
}
|
|
setTimeout(tick, 250);
|
|
});
|
|
request.on("error", () => setTimeout(tick, 250));
|
|
};
|
|
tick();
|
|
});
|
|
}
|
|
|
|
function writeTree(root) {
|
|
// Mirror user screenshot shape: root page-group + several sibling folders.
|
|
const space = path.join(root, "liaibo的个人空间");
|
|
const mk = (p, content = null) => {
|
|
if (content == null) {
|
|
fs.mkdirSync(p, { recursive: true });
|
|
return;
|
|
}
|
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
fs.writeFileSync(p, content, "utf8");
|
|
};
|
|
mk(space);
|
|
mk(path.join(space, "liaibo的个人空间.md"), "# liaibo的个人空间\n");
|
|
for (const name of ["知识", "项目", "工作资料", "日常", "剪藏", "个人"]) {
|
|
mk(path.join(space, name));
|
|
mk(path.join(space, name, `${name}.md`), `# ${name}\n`);
|
|
}
|
|
// Nested under 个人 (matches screenshot after expand)
|
|
for (const name of ["正版软件备份", "个人发展", "杂记", "密码"]) {
|
|
mk(path.join(space, "个人", name));
|
|
mk(path.join(space, "个人", name, `${name}.md`), `# ${name}\n`);
|
|
}
|
|
// Nested under 知识
|
|
for (const name of ["有机化学", "文章"]) {
|
|
mk(path.join(space, "知识", name));
|
|
mk(path.join(space, "知识", name, `${name}.md`), `# ${name}\n`);
|
|
}
|
|
return space;
|
|
}
|
|
|
|
async function signUp(context, baseUrl, actorId) {
|
|
const response = await context.request.fetch(`${baseUrl}/api/auth`, {
|
|
method: "POST",
|
|
data: {
|
|
action: "auth:signIn",
|
|
args: {
|
|
provider: "password",
|
|
params: {
|
|
email: `${actorId}@example.com`,
|
|
username: actorId,
|
|
name: actorId,
|
|
password: TEST_PASSWORD,
|
|
flow: "signUp",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.ok(response.ok(), `signUp failed: ${response.status()} ${await response.text()}`);
|
|
}
|
|
|
|
async function openDefaultLocalWorkspace(page, baseUrl) {
|
|
await page.goto(baseUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
const createButton = page.locator('[data-testid="mnote-create-default-local-workspace"]').first();
|
|
if (await createButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await createButton.click({ timeout: UI_TIMEOUT_MS });
|
|
await page.waitForURL((url) => url.searchParams.get("sourceKind") === "local_folder", {
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
}
|
|
const rootUri =
|
|
new URL(page.url()).searchParams.get("rootUri") ||
|
|
(await page.evaluate(() => document.body.getAttribute("data-mnote-root-uri") || ""));
|
|
assert.ok(rootUri, "should open local_folder workspace");
|
|
return {
|
|
rootUri,
|
|
workspaceId: new URL(page.url()).searchParams.get("workspaceId") || "",
|
|
};
|
|
}
|
|
|
|
function fileUrlToPath(rootUri) {
|
|
assert.ok(rootUri.startsWith("file://"), `unexpected rootUri ${rootUri}`);
|
|
return decodeURIComponent(rootUri.slice("file://".length));
|
|
}
|
|
|
|
async function reloadWorkspace(page, baseUrl, rootUri, workspaceId) {
|
|
const url = new URL(baseUrl);
|
|
if (workspaceId) url.searchParams.set("workspaceId", workspaceId);
|
|
url.searchParams.set("sourceKind", "local_folder");
|
|
url.searchParams.set("rootUri", rootUri);
|
|
await page.goto(url.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
await page.waitForSelector("#sidebar-tree-root .tree-row[data-shell-mode='page']", {
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.waitForTimeout(800);
|
|
}
|
|
|
|
function reportTree(page) {
|
|
return page.evaluate(() => {
|
|
function computeChevron(row) {
|
|
const toggle = row.querySelector("[data-rust-action='toggle']");
|
|
if (!toggle) return null;
|
|
const before = window.getComputedStyle(toggle, "::before");
|
|
const transform = before.transform || "";
|
|
// matrix(0,1,-1,0,0,0) ~= rotate(90deg)
|
|
const rotated =
|
|
transform.includes("matrix(0, 1, -1, 0") ||
|
|
transform.includes("matrix(0,1,-1,0") ||
|
|
(transform.includes("rotate") && !transform.includes("rotate(0"));
|
|
return { transform, rotated };
|
|
}
|
|
const rows = Array.from(
|
|
document.querySelectorAll("#sidebar-tree-root .tree-row[data-shell-mode='page']")
|
|
);
|
|
const byId = new Map();
|
|
const details = rows.map((row) => {
|
|
const nodeId = row.getAttribute("data-node-id") || "";
|
|
byId.set(nodeId, (byId.get(nodeId) || 0) + 1);
|
|
const node = row.closest(".tree-node");
|
|
const children = node && node.querySelector(":scope > .tree-children");
|
|
const toggle = row.querySelector("[data-rust-action='toggle']");
|
|
const title = String((row.querySelector(".tree-link-title") || {}).textContent || "").trim();
|
|
const chevron = computeChevron(row);
|
|
const collapsed = children ? children.classList.contains("tree-children--collapsed") : null;
|
|
const childCount = children ? children.children.length : 0;
|
|
const visuallyOpen = collapsed === false && childCount > 0;
|
|
const aria = row.getAttribute("aria-expanded");
|
|
const dataExpanded = row.getAttribute("data-expanded");
|
|
const iconSaysOpen = chevron ? chevron.rotated : null;
|
|
return {
|
|
nodeId,
|
|
title,
|
|
parentId: row.getAttribute("data-parent-id") || "",
|
|
depth: Number(row.getAttribute("data-depth") || 0),
|
|
aria,
|
|
dataExpanded,
|
|
toggleAria: toggle ? toggle.getAttribute("aria-expanded") : null,
|
|
toggleData: toggle ? toggle.getAttribute("data-expanded") : null,
|
|
collapsed,
|
|
childCount,
|
|
visuallyOpen,
|
|
iconSaysOpen,
|
|
desync:
|
|
toggle != null &&
|
|
((iconSaysOpen === true && !visuallyOpen) ||
|
|
(iconSaysOpen === false && visuallyOpen) ||
|
|
aria !== dataExpanded ||
|
|
(aria === "true") !== visuallyOpen),
|
|
};
|
|
});
|
|
const dups = [...byId.entries()].filter(([, n]) => n > 1).map(([id, n]) => ({ id, n }));
|
|
const titles = details.map((d) => d.title);
|
|
const titleCounts = titles.reduce((m, t) => {
|
|
m[t] = (m[t] || 0) + 1;
|
|
return m;
|
|
}, {});
|
|
// top-level (depth 0 or parent empty) duplicates by title are the screenshot bug
|
|
const topTitles = details.filter((d) => !d.parentId || d.depth === 0).map((d) => d.title);
|
|
const topTitleCounts = topTitles.reduce((m, t) => {
|
|
m[t] = (m[t] || 0) + 1;
|
|
return m;
|
|
}, {});
|
|
const ls = {};
|
|
for (const k of Object.keys(localStorage)) {
|
|
if (!k.includes("sidebarTreeViewState")) continue;
|
|
try {
|
|
ls[k.slice(-60)] = JSON.parse(localStorage.getItem(k) || "null");
|
|
} catch {
|
|
ls[k.slice(-60)] = localStorage.getItem(k);
|
|
}
|
|
}
|
|
return {
|
|
href: location.href,
|
|
rowCount: rows.length,
|
|
dups,
|
|
topTitleCounts,
|
|
desync: details.filter((d) => d.desync),
|
|
openRows: details.filter((d) => d.visuallyOpen).map((d) => d.title),
|
|
sample: details,
|
|
markers: {
|
|
applied: document.documentElement.getAttribute("data-mnote-page-tree-view-state-applied"),
|
|
synced: document.documentElement.getAttribute("data-mnote-page-tree-expand-synced"),
|
|
restored: document.documentElement.getAttribute("data-mnote-page-tree-expansion-restored"),
|
|
saved: document.documentElement.getAttribute("data-mnote-sidebar-tree-view-state-saved"),
|
|
loadErr: document.documentElement.getAttribute("data-mnote-sidebar-tree-view-state-load-error"),
|
|
saveErr: document.documentElement.getAttribute("data-mnote-sidebar-tree-view-state-save-error"),
|
|
},
|
|
ls,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function clickToggleByTitle(page, title) {
|
|
return page.evaluate((want) => {
|
|
const rows = Array.from(
|
|
document.querySelectorAll("#sidebar-tree-root .tree-row[data-shell-mode='page']")
|
|
);
|
|
for (const row of rows) {
|
|
const t = String((row.querySelector(".tree-link-title") || {}).textContent || "").trim();
|
|
if (t !== want) continue;
|
|
const btn = row.querySelector("[data-rust-action='toggle']");
|
|
if (!btn) return { ok: false, reason: "no_toggle" };
|
|
btn.click();
|
|
return { ok: true, nodeId: row.getAttribute("data-node-id") };
|
|
}
|
|
return { ok: false, reason: "not_found" };
|
|
}, title);
|
|
}
|
|
|
|
async function waitDesyncClear(page, timeoutMs = 3000) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
const r = await reportTree(page);
|
|
if (r.desync.length === 0) return r;
|
|
await page.waitForTimeout(100);
|
|
}
|
|
return reportTree(page);
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(RESULT_DIR, { recursive: true });
|
|
if (!fs.existsSync(BINARY)) {
|
|
throw new Error(`binary missing: ${BINARY} (build mnote-web first)`);
|
|
}
|
|
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-expand-reg-"));
|
|
const port = await pickPort();
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
const cpEnv = buildControlPlaneTestEnv(dataRoot);
|
|
const env = {
|
|
...process.env,
|
|
...cpEnv,
|
|
MNOTE_LOCAL_WORKSPACE_BASE_DIR: dataRoot,
|
|
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
|
|
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
|
|
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
|
RUST_LOG: process.env.RUST_LOG || "warn",
|
|
};
|
|
const child = spawn(BINARY, [], {
|
|
cwd: path.join(__dirname, "..", "rust"),
|
|
env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let serverLog = "";
|
|
child.stdout.on("data", (d) => {
|
|
serverLog += d.toString();
|
|
});
|
|
child.stderr.on("data", (d) => {
|
|
serverLog += d.toString();
|
|
});
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: resolveChromiumExecutablePath() || undefined,
|
|
});
|
|
const results = { steps: [] };
|
|
try {
|
|
await waitForHttpOk(`${baseUrl}/health`, 45_000);
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
const actorId = `expand-reg-${Date.now()}`;
|
|
await signUp(context, baseUrl, actorId);
|
|
const page = await context.newPage();
|
|
const puts = [];
|
|
const gets = [];
|
|
page.on("response", async (res) => {
|
|
if (!res.url().includes("/api/tree/view-state")) return;
|
|
const method = res.request().method();
|
|
let body = null;
|
|
try {
|
|
body = await res.json();
|
|
} catch {}
|
|
const entry = {
|
|
method,
|
|
status: res.status(),
|
|
expandedIds: body && body.result && body.result.state && body.result.state.expandedIds,
|
|
};
|
|
if (method === "PUT") puts.push(entry);
|
|
else gets.push(entry);
|
|
});
|
|
|
|
const { rootUri, workspaceId } = await openDefaultLocalWorkspace(page, baseUrl);
|
|
const rootPath = fileUrlToPath(rootUri);
|
|
// Seed liaibo-like tree into the default local workspace.
|
|
writeTree(rootPath);
|
|
await reloadWorkspace(page, baseUrl, rootUri, workspaceId);
|
|
await page.screenshot({ path: path.join(RESULT_DIR, "01-initial.png") });
|
|
let r = await reportTree(page);
|
|
results.steps.push({ name: "initial", r });
|
|
assert.equal(r.dups.length, 0, `initial dups: ${JSON.stringify(r.dups)}`);
|
|
assert.equal(r.desync.length, 0, `initial desync: ${JSON.stringify(r.desync)}`);
|
|
// Default: expandable rows closed + ▶ (ignore non-toggle leaves)
|
|
for (const d of r.sample.filter((x) => x.toggleAria != null)) {
|
|
assert.equal(d.visuallyOpen, false, `initial open ${d.title}`);
|
|
assert.equal(d.iconSaysOpen, false, `initial icon open ${d.title}`);
|
|
assert.equal(d.aria, "false", `initial aria ${d.title}`);
|
|
}
|
|
|
|
// Expand root then 个人
|
|
let click = await clickToggleByTitle(page, "liaibo的个人空间");
|
|
assert.equal(click.ok, true, `expand root: ${JSON.stringify(click)}`);
|
|
await page.waitForTimeout(700);
|
|
click = await clickToggleByTitle(page, "个人");
|
|
assert.equal(click.ok, true, `expand 个人: ${JSON.stringify(click)}`);
|
|
await page.waitForTimeout(900);
|
|
await page.waitForTimeout(700);
|
|
await page.screenshot({ path: path.join(RESULT_DIR, "02-expanded.png") });
|
|
r = await waitDesyncClear(page);
|
|
results.steps.push({ name: "expanded", r, puts: puts.slice(-2) });
|
|
assert.equal(r.dups.length, 0, `expanded dups: ${JSON.stringify(r.dups)}`);
|
|
assert.equal(r.desync.length, 0, `expanded desync: ${JSON.stringify(r.desync)}`);
|
|
assert.ok(r.openRows.includes("liaibo的个人空间"), "root should be open");
|
|
assert.ok(r.openRows.includes("个人"), "个人 should be open");
|
|
const personalChildren = r.sample.filter((d) => d.title === "正版软件备份");
|
|
assert.equal(personalChildren.length, 1, `个人 children dup: ${JSON.stringify(personalChildren)}`);
|
|
|
|
// Collapse 个人 only
|
|
click = await clickToggleByTitle(page, "个人");
|
|
assert.equal(click.ok, true);
|
|
await page.waitForTimeout(500);
|
|
r = await waitDesyncClear(page);
|
|
results.steps.push({ name: "collapsed-personal", r });
|
|
assert.equal(r.dups.length, 0, `collapse dups ${JSON.stringify(r.dups)}`);
|
|
assert.equal(r.desync.length, 0, `collapse desync ${JSON.stringify(r.desync)}`);
|
|
const personal = r.sample.find((d) => d.title === "个人");
|
|
assert.ok(personal, "个人 row missing");
|
|
assert.equal(personal.visuallyOpen, false, "个人 should be closed");
|
|
assert.equal(personal.iconSaysOpen, false, "个人 icon should be ▶");
|
|
assert.equal(personal.aria, "false");
|
|
const titleCountsUnderRoot = {};
|
|
for (const d of r.sample) {
|
|
if (d.depth === 1) titleCountsUnderRoot[d.title] = (titleCountsUnderRoot[d.title] || 0) + 1;
|
|
}
|
|
for (const [title, count] of Object.entries(titleCountsUnderRoot)) {
|
|
assert.equal(count, 1, `duplicate depth1 title ${title} count=${count}`);
|
|
}
|
|
|
|
// Re-expand 个人, then reload — should restore open state
|
|
click = await clickToggleByTitle(page, "个人");
|
|
assert.equal(click.ok, true);
|
|
await page.waitForTimeout(1000);
|
|
r = await reportTree(page);
|
|
const lsValues = Object.values(r.ls);
|
|
const hasLocal =
|
|
lsValues.some((v) => v && Array.isArray(v.expandedIds) && v.expandedIds.length > 0) ||
|
|
puts.some((p) => p.status === 200);
|
|
assert.ok(hasLocal || puts.length > 0, `persist missing ls=${JSON.stringify(r.ls)} puts=${puts.length}`);
|
|
|
|
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
await page.waitForSelector("#sidebar-tree-root .tree-row[data-shell-mode='page']", {
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.waitForTimeout(2500);
|
|
await page.screenshot({ path: path.join(RESULT_DIR, "03-after-reload.png") });
|
|
r = await waitDesyncClear(page, 5000);
|
|
results.steps.push({ name: "after-reload-open", r, gets: gets.slice(-4), puts: puts.slice(-2) });
|
|
assert.equal(r.dups.length, 0, `reload dups: ${JSON.stringify(r.dups)}`);
|
|
assert.equal(r.desync.length, 0, `reload desync: ${JSON.stringify(r.desync)}`);
|
|
assert.ok(
|
|
r.openRows.includes("liaibo的个人空间") && r.openRows.includes("个人"),
|
|
`reload should restore expands, openRows=${JSON.stringify(r.openRows)} markers=${JSON.stringify(r.markers)} ls=${JSON.stringify(r.ls)}`
|
|
);
|
|
// Nested children of 个人 must actually reappear after cascade hydrate.
|
|
const afterReloadPersonalChild = r.sample.filter((d) => d.title === "正版软件备份");
|
|
assert.equal(
|
|
afterReloadPersonalChild.length,
|
|
1,
|
|
`reload missing nested child: ${JSON.stringify(afterReloadPersonalChild)} sample=${JSON.stringify(
|
|
r.sample.map((d) => ({ t: d.title, d: d.depth, open: d.visuallyOpen, children: d.childCount }))
|
|
)}`
|
|
);
|
|
const personalAfterReload = r.sample.find((d) => d.title === "个人");
|
|
assert.ok(personalAfterReload && personalAfterReload.childCount >= 4, `个人 childCount after reload ${personalAfterReload?.childCount}`);
|
|
assert.equal(personalAfterReload.iconSaysOpen, true, "个人 icon should be ▼ after restore");
|
|
const d1 = {};
|
|
for (const d of r.sample) {
|
|
if (d.depth === 1) d1[d.title] = (d1[d.title] || 0) + 1;
|
|
}
|
|
for (const [title, count] of Object.entries(d1)) {
|
|
assert.equal(count, 1, `reload duplicate depth1 ${title}=${count}`);
|
|
}
|
|
|
|
// Collapse all expanded, reload — icons must stay ▶
|
|
for (const title of ["个人", "liaibo的个人空间"]) {
|
|
const row = r.sample.find((d) => d.title === title);
|
|
if (row && row.visuallyOpen) {
|
|
await clickToggleByTitle(page, title);
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
await page.waitForTimeout(900);
|
|
r = await waitDesyncClear(page);
|
|
results.steps.push({ name: "all-collapsed", r });
|
|
assert.equal(r.desync.length, 0, `all-collapsed desync ${JSON.stringify(r.desync)}`);
|
|
for (const d of r.sample.filter((x) => x.toggleAria != null)) {
|
|
assert.equal(d.visuallyOpen, false, `should be closed ${d.title}`);
|
|
assert.equal(d.iconSaysOpen, false, `icon should be ▶ for ${d.title}`);
|
|
}
|
|
|
|
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
await page.waitForSelector("#sidebar-tree-root .tree-row[data-shell-mode='page']", {
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
await page.waitForTimeout(2500);
|
|
await page.screenshot({ path: path.join(RESULT_DIR, "04-reload-collapsed.png") });
|
|
r = await waitDesyncClear(page, 5000);
|
|
results.steps.push({ name: "after-reload-collapsed", r });
|
|
assert.equal(r.dups.length, 0, `collapsed-reload dups ${JSON.stringify(r.dups)}`);
|
|
assert.equal(r.desync.length, 0, `collapsed-reload desync ${JSON.stringify(r.desync)}`);
|
|
assert.deepEqual(r.openRows, [], `collapsed reload openRows=${JSON.stringify(r.openRows)}`);
|
|
for (const d of r.sample.filter((x) => x.toggleAria != null)) {
|
|
assert.equal(d.iconSaysOpen, false, `collapsed reload icon open for ${d.title}`);
|
|
assert.equal(d.aria, "false", `collapsed reload aria for ${d.title}`);
|
|
assert.equal(d.dataExpanded, "false", `collapsed reload data-expanded for ${d.title}`);
|
|
}
|
|
|
|
results.ok = true;
|
|
results.puts = puts;
|
|
results.gets = gets.slice(-6);
|
|
fs.writeFileSync(path.join(RESULT_DIR, "result.json"), JSON.stringify(results, null, 2));
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
resultDir: RESULT_DIR,
|
|
openAfterExpand: results.steps.find((s) => s.name === "expanded")?.r?.openRows,
|
|
afterReload: results.steps.find((s) => s.name === "after-reload-open")?.r?.openRows,
|
|
afterCollapsedReload: results.steps.find((s) => s.name === "after-reload-collapsed")?.r
|
|
?.openRows,
|
|
putCount: puts.length,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
} catch (error) {
|
|
results.ok = false;
|
|
results.error = String((error && error.stack) || error);
|
|
fs.writeFileSync(path.join(RESULT_DIR, "result.json"), JSON.stringify(results, null, 2));
|
|
fs.writeFileSync(path.join(RESULT_DIR, "server.log"), serverLog);
|
|
console.error(results.error);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await browser.close().catch(() => {});
|
|
child.kill("SIGTERM");
|
|
setTimeout(() => {
|
|
try {
|
|
child.kill("SIGKILL");
|
|
} catch {}
|
|
}, 1000).unref();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|