feat: cut over rust web main shell
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
// 说明:
|
||||
// - 这份 smoke 用来覆盖 tree rust family checklist 第 9 节的通用回归要求。
|
||||
// - 宿主页负责验证真实导航、上下文菜单、picker 对话框与 stream fallback。
|
||||
// - legacy React island 负责验证真实导航、上下文菜单、picker 对话框;3000 Rust gateway 负责 API 与 stream。
|
||||
// - `/api/tree/shell` 直连页已退到显式 debug/internal 边界,默认不再进入这条链路。
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
DOCUMENT_UI_BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
assert,
|
||||
createTempDocument,
|
||||
@@ -25,6 +26,7 @@ const RUN_DIRECT_TREE_SHELL_CHECKS =
|
||||
String(process.env.MNOTE_TREE_SHELL_DIRECT_SMOKE || "").trim() === "1";
|
||||
const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST =
|
||||
String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1";
|
||||
const CHROMIUM_STABLE_ARGS = ["--disable-dev-shm-usage", "--disable-gpu"];
|
||||
|
||||
function buildTreeShellUrl(workspaceId, params = {}) {
|
||||
const search = new URLSearchParams({
|
||||
@@ -302,10 +304,19 @@ async function openPageTreeContextMenuFromHost(page, driver, documentId) {
|
||||
} else {
|
||||
const row = driver.scope.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`);
|
||||
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await row.locator('[data-testid="tree-action-menu"]').click({ timeout: UI_TIMEOUT_MS, force: true });
|
||||
const menuButton = row.locator('[data-testid="tree-action-menu"]');
|
||||
await menuButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
|
||||
await menuButton.evaluate((button) => {
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
throw new Error("页面树更多操作按钮不存在");
|
||||
}
|
||||
button.click();
|
||||
});
|
||||
}
|
||||
await page.getByText("重命名", { exact: true }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.evaluate(() => {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
async function doubleClickFileTreeDocumentFromHost(page, driver, documentId) {
|
||||
@@ -874,6 +885,11 @@ async function runFileTreeHostDoubleClickCheck(context, fixture) {
|
||||
}
|
||||
}
|
||||
|
||||
function isPlaywrightTargetCrash(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes("Target crashed");
|
||||
}
|
||||
|
||||
async function waitForPickerEmptyState(page, dialog, pickerUsesIframe) {
|
||||
if (!pickerUsesIframe) {
|
||||
const emptyText = dialog.getByText("没有匹配结果");
|
||||
@@ -1113,56 +1129,29 @@ async function runPickerDialogChecks(context, fixture) {
|
||||
}
|
||||
|
||||
async function runStreamFallbackCheck(context, fixture) {
|
||||
const page = await context.newPage();
|
||||
let streamRequestCount = 0;
|
||||
const snapshot = await requestJson(context.request, `/api/sidebar?workspaceId=${encodeURIComponent(fixture.workspaceId)}`);
|
||||
const response = await context.request.fetch(
|
||||
`${BASE_URL}/api/tree/events?workspaceId=${encodeURIComponent(fixture.workspaceId)}&maxPolls=0`,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const body = await response.text();
|
||||
assert(response.ok(), `/api/tree/events 请求失败: ${response.status()} ${body.slice(0, 200)}`);
|
||||
assert(
|
||||
(response.headers()["content-type"] || "").includes("text/event-stream"),
|
||||
"/api/tree/events 未返回 SSE 内容类型",
|
||||
);
|
||||
assert(response.headers()["x-mnote-web-owner"] === "mnote-web", "/api/tree/events 缺少 mnote-web owner header");
|
||||
assert(
|
||||
response.headers()["x-mnote-tree-stream-owner"] === "rust-web",
|
||||
"/api/tree/events 缺少 rust-web stream owner header",
|
||||
);
|
||||
assert(body.includes("event: snapshot") || body.includes("event:snapshot"), "/api/tree/events 未返回 snapshot event");
|
||||
assert(body.includes('"kind":"snapshot"'), "/api/tree/events snapshot payload 缺少 kind=snapshot");
|
||||
|
||||
await page.route("**/api/mnote-web/stream**", async (route) => {
|
||||
streamRequestCount += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
body: buildSnapshotSseBody(snapshot, fixture.workspaceId),
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await openDocument(page, fixture.workspaceId, fixture.parentId);
|
||||
await waitForPageTitleInput(page);
|
||||
await page.waitForTimeout(600);
|
||||
assert(streamRequestCount > 0, "stream fallback smoke 未命中 /api/mnote-web/stream");
|
||||
|
||||
const sidebarRefetchResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/sidebar?workspaceId=${encodeURIComponent(fixture.workspaceId)}`) &&
|
||||
response.request().method() === "GET" &&
|
||||
response.ok(),
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await renameThroughPageHead(page, fixture.parentId, fixture.fallbackTitle);
|
||||
await sidebarRefetchResponse;
|
||||
|
||||
await waitForBreadcrumbTitle(page, fixture.fallbackTitle);
|
||||
await page.waitForFunction(
|
||||
(expectedTitle) => {
|
||||
const input = document.querySelector('input[aria-label="页面标题"]');
|
||||
return input instanceof HTMLInputElement && input.value.includes(expectedTitle);
|
||||
},
|
||||
fixture.fallbackTitle,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
return {
|
||||
streamFallback: true,
|
||||
streamRequestCount,
|
||||
fallbackTitle: fixture.fallbackTitle,
|
||||
};
|
||||
} finally {
|
||||
await page.close().catch(() => undefined);
|
||||
}
|
||||
return {
|
||||
streamFallback: false,
|
||||
rustTreeEvents: true,
|
||||
streamRequestCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -1172,11 +1161,12 @@ async function main() {
|
||||
: process.env.MNOTE_SMOKE_HEADLESS === "0"
|
||||
? false
|
||||
: !process.env.DISPLAY;
|
||||
const browser = await chromium.launch({ headless });
|
||||
const context = await browser.newContext({
|
||||
const browser = await chromium.launch({ headless, args: CHROMIUM_STABLE_ARGS });
|
||||
const browserContextOptions = {
|
||||
viewport: { width: 1440, height: 960 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
};
|
||||
let context = await browser.newContext(browserContextOptions);
|
||||
let page = await context.newPage();
|
||||
|
||||
let fixture = null;
|
||||
let caughtError = null;
|
||||
@@ -1186,13 +1176,35 @@ async function main() {
|
||||
fixture = await prepareFixture(context.request);
|
||||
|
||||
const pageTreeHost = await runPageTreeHostChecks(page, fixture);
|
||||
const storageState = await context.storageState();
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
context = await browser.newContext({
|
||||
...browserContextOptions,
|
||||
storageState,
|
||||
});
|
||||
page = null;
|
||||
const pageTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
|
||||
? await runPageTreeShellChecks(context, fixture)
|
||||
: { skipped: true, reason: "direct_tree_shell_debug_disabled" };
|
||||
const fileTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
|
||||
? await runFileTreeShellChecks(context, fixture)
|
||||
: { skipped: true, reason: "direct_tree_shell_debug_disabled" };
|
||||
const fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
|
||||
let fileTreeHost;
|
||||
try {
|
||||
fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
|
||||
} catch (error) {
|
||||
if (!isPlaywrightTargetCrash(error)) {
|
||||
throw error;
|
||||
}
|
||||
await context.close().catch(() => undefined);
|
||||
context = await browser.newContext({
|
||||
...browserContextOptions,
|
||||
storageState,
|
||||
});
|
||||
fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
|
||||
fileTreeHost.recoveredFromRendererCrash = true;
|
||||
}
|
||||
const picker = await runPickerDialogChecks(context, fixture);
|
||||
const streamFallback = await runStreamFallbackCheck(context, fixture);
|
||||
|
||||
@@ -1201,6 +1213,7 @@ async function main() {
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
documentBaseUrl: DOCUMENT_UI_BASE_URL,
|
||||
viewerUserId: viewer.userId,
|
||||
workspaceId: fixture.workspaceId,
|
||||
parentId: fixture.parentId,
|
||||
@@ -1241,7 +1254,9 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
await page.close().catch(() => undefined);
|
||||
if (page) {
|
||||
await page.close().catch(() => undefined);
|
||||
}
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user