feat: integrate todo note microservice and modularize frontend
This commit is contained in:
+3
-3
@@ -22,11 +22,11 @@ function help(): unknown {
|
||||
{ command: "--main-server-ip <ip> <command>", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." },
|
||||
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
|
||||
{ command: "check", description: "Run config, TypeScript, file presence, and docker-compose config checks." },
|
||||
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, and provider gateway." },
|
||||
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server microservices." },
|
||||
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
|
||||
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
|
||||
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
|
||||
{ command: "server rebuild <backend-core|frontend|provider-gateway>", description: "Build first, then label-replace one service without Docker Compose v1 recreate fallback." },
|
||||
{ command: "server rebuild <backend-core|frontend|provider-gateway|todo-note>", description: "Build first, then label-replace one service without Docker Compose v1 recreate fallback." },
|
||||
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge." },
|
||||
{ command: "microservice list", description: "List UniDesk-managed microservices and their provider/runtime mapping." },
|
||||
{ command: "microservice status <id>", description: "Show one microservice config, repository reference, backend mapping, and runtime status." },
|
||||
@@ -149,7 +149,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (sub === "rebuild") {
|
||||
if (!isRebuildableService(third)) {
|
||||
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway");
|
||||
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note");
|
||||
}
|
||||
emitJson(commandName, rebuildService(config, third));
|
||||
return;
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface UniDeskMicroserviceConfig {
|
||||
proxyMode: string;
|
||||
frontendOnly: boolean;
|
||||
public: boolean;
|
||||
allowedMethods: string[];
|
||||
allowedPathPrefixes: string[];
|
||||
healthPath: string;
|
||||
timeoutMs: number;
|
||||
@@ -152,6 +153,7 @@ function microserviceConfig(item: Record<string, unknown>, index: number): UniDe
|
||||
proxyMode: stringField(backend, "proxyMode", `${path}.backend`),
|
||||
frontendOnly: booleanField(backend, "frontendOnly", `${path}.backend`),
|
||||
public: booleanField(backend, "public", `${path}.backend`),
|
||||
allowedMethods: stringArrayField(backend, "allowedMethods", `${path}.backend`),
|
||||
allowedPathPrefixes: stringArrayField(backend, "allowedPathPrefixes", `${path}.backend`),
|
||||
healthPath: stringField(backend, "healthPath", `${path}.backend`),
|
||||
timeoutMs: numberField(backend, "timeoutMs", `${path}.backend`),
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface ContainerStatus {
|
||||
ports: string;
|
||||
}
|
||||
|
||||
const rebuildableServices = ["backend-core", "frontend", "provider-gateway"] as const;
|
||||
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note"] as const;
|
||||
export type RebuildableService = typeof rebuildableServices[number];
|
||||
|
||||
export function isRebuildableService(value: string | undefined): value is RebuildableService {
|
||||
@@ -79,6 +79,8 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_DATABASE_USER: config.database.user,
|
||||
UNIDESK_DATABASE_PASSWORD: config.database.password,
|
||||
UNIDESK_DATABASE_NAME: config.database.name,
|
||||
UNIDESK_DATABASE_VOLUME: config.database.volume,
|
||||
UNIDESK_DATABASE_VOLUME_SIZE: config.database.volumeSize,
|
||||
UNIDESK_PROVIDER_TOKEN: config.providerGateway.token,
|
||||
UNIDESK_PROVIDER_ID: config.providerGateway.id,
|
||||
UNIDESK_PROVIDER_NAME: config.providerGateway.name,
|
||||
@@ -130,7 +132,7 @@ export function startStack(config: UniDeskConfig): unknown {
|
||||
const downCommand = [...compose, "down", "--remove-orphans"];
|
||||
const upCommand = [...compose, "up", "-d", "--build"];
|
||||
const command = ["bash", "-lc", `set -euo pipefail; ${shellJoin(downCommand)}; ${shellJoin(upCommand)}`];
|
||||
const job = startJob("server_start", command, "Build and start UniDesk database, core, frontend, and provider gateway containers");
|
||||
const job = startJob("server_start", command, "Build and start UniDesk database, core, frontend, provider gateway, and managed microservice containers");
|
||||
return { job, runtimeEnv, command, ports: fixedPorts(config) };
|
||||
}
|
||||
|
||||
@@ -301,7 +303,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
|
||||
const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
|
||||
const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
|
||||
const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) }));
|
||||
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main"];
|
||||
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend"];
|
||||
const docker = containerNames.map((name) => {
|
||||
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
|
||||
return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) };
|
||||
|
||||
+101
-2
@@ -222,10 +222,12 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
|
||||
const corePublic = await fetchProbe(`${urls.blockedCoreUrl}/health`, 2500);
|
||||
const databasePublic = await tcpProbe(urls.blockedDatabaseHost, urls.blockedDatabasePort);
|
||||
const findjobPublic = await fetchProbe(`http://${config.network.publicHost}:3254/api/health`, 2500);
|
||||
const todoNotePublic = await fetchProbe(`http://${config.network.publicHost}:4211/api/health`, 2500);
|
||||
addCheck(checks, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(`:${config.network.database.port}->`), portSummary);
|
||||
addCheck(checks, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic);
|
||||
addCheck(checks, "network:database-public-blocked", (databasePublic as { reachable?: boolean }).reachable === false, databasePublic);
|
||||
addCheck(checks, "network:findjob-public-blocked", (findjobPublic as { reachable?: boolean }).reachable === false, findjobPublic);
|
||||
addCheck(checks, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic);
|
||||
}
|
||||
|
||||
async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
@@ -241,8 +243,28 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const pipelineStatus = dockerCoreJson("/api/microservices/pipeline/status");
|
||||
const pipelineHealth = dockerCoreJson("/api/microservices/pipeline/health");
|
||||
const pipelineSnapshot = dockerCoreJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3");
|
||||
const todoNoteStatus = dockerCoreJson("/api/microservices/todo-note/status");
|
||||
const todoNoteHealth = dockerCoreJson("/api/microservices/todo-note/health");
|
||||
const todoNoteInstances = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances");
|
||||
const todoE2eName = `E2E Todo ${Date.now()}`;
|
||||
const todoNoteCreate = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances", { method: "POST", body: { name: todoE2eName } });
|
||||
const todoCreatedId = (todoNoteCreate as { body?: { id?: string } }).body?.id ?? "";
|
||||
const todoNoteAdd = todoCreatedId
|
||||
? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "addTodo", title: "E2E migrated write path" } } })
|
||||
: { ok: false, error: "missing created todo note id", todoNoteCreate };
|
||||
const todoAddedItems = (todoNoteAdd as { body?: { todos?: Array<{ id?: string }> } }).body?.todos ?? [];
|
||||
const todoAddedId = todoAddedItems[0]?.id ?? "";
|
||||
const todoNoteToggle = todoCreatedId && todoAddedId
|
||||
? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "toggleTodoCompleted", todoId: todoAddedId } } })
|
||||
: { ok: false, error: "missing todo id", todoNoteAdd };
|
||||
const todoNoteUndo = todoCreatedId
|
||||
? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/undo`, { method: "POST", body: {} })
|
||||
: { ok: false, error: "missing created todo note id" };
|
||||
const todoNoteDelete = todoCreatedId
|
||||
? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}`, { method: "DELETE" })
|
||||
: { ok: false, error: "missing created todo note id" };
|
||||
const providerIngress = await fetchProbe(urls.providerIngressHealthUrl);
|
||||
const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number } }).body;
|
||||
const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number; pgdata?: { volumeName?: string; databaseBytes?: number } } }).body;
|
||||
const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string; labels?: Record<string, unknown> }> } }).body?.nodes ?? [];
|
||||
const mainNode = nodeList.find((node) => node.providerId === config.providerGateway.id);
|
||||
const expectedGatewayVersion = providerGatewayPackageVersion();
|
||||
@@ -251,6 +273,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? [];
|
||||
const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id);
|
||||
addCheck(checks, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview);
|
||||
addCheck(checks, "core:pgdata-usage", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.pgdata?.volumeName === config.database.volume && Number(overviewBody.pgdata.databaseBytes ?? 0) > 0, coreOverview);
|
||||
addCheck(checks, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes);
|
||||
addCheck(checks, "provider:gateway-version-label", mainNode?.labels?.providerGatewayVersion === expectedGatewayVersion && mainNode?.labels?.providerGatewayUpgradePolicy === "always-enabled", { providerId: config.providerGateway.id, expectedGatewayVersion, labels: mainNode?.labels ?? null });
|
||||
addCheck(checks, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && mainSystem.current.memory?.mode === "actual_without_cache" && Number.isFinite(mainSystem.current.memory?.cacheBytes) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id));
|
||||
@@ -258,8 +281,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const microserviceList = (microservices as { body?: { microservices?: Array<{ id?: string; providerId?: string; backend?: { public?: boolean }; runtime?: { providerStatus?: string; container?: { name?: string; state?: string } } }> } }).body?.microservices ?? [];
|
||||
const findjob = microserviceList.find((service) => service.id === "findjob");
|
||||
const pipeline = microserviceList.find((service) => service.id === "pipeline");
|
||||
const todoNote = microserviceList.find((service) => service.id === "todo-note");
|
||||
const findjobSummaryBody = (findjobSummary as { body?: { totalJobs?: number; prioritizedJobs?: number } }).body;
|
||||
const findjobJobs = (findjobJobsPreview as { body?: { jobs?: unknown[]; _unidesk?: { arrayLimits?: { jobs?: { returnedLength?: number; originalLength?: number } } } } }).body;
|
||||
const todoNoteRows = (todoNoteInstances as { body?: { instances?: Array<{ id?: string; name?: string; todoCount?: number; completedCount?: number }> } }).body?.instances ?? [];
|
||||
const todoNoteNames = todoNoteRows.map((row) => row.name);
|
||||
const pipelineSnapshotBody = (pipelineSnapshot as { body?: { ok?: boolean; registry?: { ok?: boolean; components?: unknown[] }; pipelines?: unknown[]; runs?: unknown[]; _unidesk?: { arrayLimits?: { "registry.components"?: { returnedLength?: number; originalLength?: number }; runs?: { returnedLength?: number; originalLength?: number } } } } }).body;
|
||||
const firstPipelineRun = Array.isArray(pipelineSnapshotBody?.runs)
|
||||
? pipelineSnapshotBody.runs[0] as { runId?: string; pipelineId?: string; status?: string; updatedAt?: string } | undefined
|
||||
@@ -284,6 +310,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
};
|
||||
addCheck(checks, "microservice:catalog-findjob", (microservices as { ok?: boolean }).ok === true && findjob?.providerId === "D601" && findjob.backend?.public === false, { microservices });
|
||||
addCheck(checks, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-webui", { microservices });
|
||||
addCheck(checks, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices });
|
||||
addCheck(checks, "microservice:findjob-status", (findjobStatus as { ok?: boolean }).ok === true && (findjobStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", findjobStatus);
|
||||
addCheck(checks, "microservice:findjob-health", (findjobHealth as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (findjobHealth as { body?: { ok?: boolean } }).body?.ok === true, findjobHealth);
|
||||
addCheck(checks, "microservice:findjob-summary", (findjobSummary as { ok?: boolean }).ok === true && Number.isFinite(findjobSummaryBody?.totalJobs) && Number.isFinite(findjobSummaryBody?.prioritizedJobs), findjobSummary);
|
||||
@@ -291,6 +318,10 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addCheck(checks, "microservice:pipeline-status", (pipelineStatus as { ok?: boolean }).ok === true && (pipelineStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", pipelineStatus);
|
||||
addCheck(checks, "microservice:pipeline-health", (pipelineHealth as { ok?: boolean; body?: { ok?: boolean; service?: string } }).ok === true && (pipelineHealth as { body?: { ok?: boolean } }).body?.ok === true, pipelineHealth);
|
||||
addCheck(checks, "microservice:pipeline-snapshot", (pipelineSnapshot as { ok?: boolean }).ok === true && pipelineSnapshotBody?.ok === true && pipelineSnapshotBody.registry?.ok === true && Array.isArray(pipelineSnapshotBody.registry.components) && pipelineSnapshotBody.registry.components.length > 0 && Array.isArray(pipelineSnapshotBody.pipelines) && pipelineSnapshotBody.pipelines.length > 0 && Array.isArray(pipelineSnapshotBody.runs) && pipelineSnapshotBody.runs.length > 0 && (pipelineSnapshotBody._unidesk?.arrayLimits?.["registry.components"]?.returnedLength ?? 999) <= 8 && (pipelineSnapshotBody._unidesk?.arrayLimits?.runs?.returnedLength ?? 999) <= 3, pipelineSnapshotDetail);
|
||||
addCheck(checks, "microservice:todo-note-status", (todoNoteStatus as { ok?: boolean }).ok === true && (todoNoteStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, todoNoteStatus);
|
||||
addCheck(checks, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth);
|
||||
addCheck(checks, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances });
|
||||
addCheck(checks, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete });
|
||||
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
|
||||
method: "POST",
|
||||
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
|
||||
@@ -327,10 +358,27 @@ function databaseChecks(config: UniDeskConfig, checks: E2ECheck[]): string {
|
||||
SELECT 'marker_count=' || count(*) FROM unidesk_e2e_markers;
|
||||
SELECT 'failed_task=' || id FROM unidesk_tasks WHERE id = '${failedTaskId}' AND status = 'failed';
|
||||
SELECT 'online_main_server=' || count(*) FROM unidesk_nodes WHERE provider_id = '${config.providerGateway.id}' AND status = 'online';
|
||||
SELECT 'todo_note_instances=' || count(*) FROM todo_note_instances;
|
||||
WITH RECURSIVE todo_tree(instance_id, todo) AS (
|
||||
SELECT id, jsonb_array_elements(todos) FROM todo_note_instances
|
||||
UNION ALL
|
||||
SELECT todo_tree.instance_id, child.value
|
||||
FROM todo_tree
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(todo_tree.todo->'children') = 'array' THEN todo_tree.todo->'children'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS child(value)
|
||||
)
|
||||
SELECT 'todo_note_total_todos=' || count(*) FROM todo_tree;
|
||||
`;
|
||||
const marker = runPsql(config, markerSql);
|
||||
const todoNoteInstanceCount = Number(marker.stdout.match(/todo_note_instances=(\d+)/)?.[1] ?? NaN);
|
||||
const todoNoteTotalTodos = Number(marker.stdout.match(/todo_note_total_todos=(\d+)/)?.[1] ?? NaN);
|
||||
addCheck(checks, "database:named-volume-write", marker.ok && marker.stdout.includes(`marker=${markerId}`), marker);
|
||||
addCheck(checks, "database:provider-state", marker.ok && marker.stdout.includes("online_main_server=1"), marker);
|
||||
addCheck(checks, "database:todo-note-pg-storage", marker.ok && todoNoteInstanceCount >= 5 && todoNoteTotalTodos >= 100, { ...marker, todoNoteInstanceCount, todoNoteTotalTodos });
|
||||
return markerId;
|
||||
}
|
||||
|
||||
@@ -346,6 +394,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
if (message.type() === "error") consoleErrors.push(message.text());
|
||||
});
|
||||
page.on("pageerror", (error) => consoleErrors.push(error.message));
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForSelector('[data-testid="login-screen"]', { timeout: 10000 });
|
||||
await page.fill('input[name="username"]', config.auth.username);
|
||||
@@ -359,6 +408,12 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const publicFrontendReached = landed.origin === publicOrigin && !["127.0.0.1", "localhost", "::1"].includes(landed.hostname);
|
||||
await page.waitForSelector(`text=${config.providerGateway.id}`, { timeout: 10000 });
|
||||
await page.waitForSelector(`text=${config.providerGateway.name}`, { timeout: 10000 });
|
||||
const railWidthBefore = await page.locator(".rail").evaluate((element) => Math.round(element.getBoundingClientRect().width));
|
||||
await page.getByTestId("rail-toggle").click();
|
||||
await page.waitForTimeout(120);
|
||||
const railWidthCollapsed = await page.locator(".rail").evaluate((element) => Math.round(element.getBoundingClientRect().width));
|
||||
await page.getByTestId("rail-toggle").click();
|
||||
await page.waitForTimeout(80);
|
||||
await page.setViewportSize({ width: 390, height: 860 });
|
||||
const mobileRailHeights: number[] = [];
|
||||
for (const moduleLabel of ["运行总览", "资源节点", "任务调度", "微服务", "系统配置"]) {
|
||||
@@ -441,7 +496,43 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-findjob"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-pipeline"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 });
|
||||
const microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: /Todo Note/ }).click();
|
||||
await page.waitForSelector('[data-testid="todo-note-page"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText;
|
||||
const lower = text.toLowerCase();
|
||||
return lower.includes("todo note 工作台")
|
||||
&& text.includes("CONSTAR")
|
||||
&& text.includes("大论文")
|
||||
&& text.includes("找工作")
|
||||
&& text.includes("小论文")
|
||||
&& text.includes("事务")
|
||||
&& text.includes("仅 UniDesk frontend 代理访问");
|
||||
}, undefined, { timeout: 30000 });
|
||||
const uiTodoListName = `UI E2E ${Date.now()}`;
|
||||
await page.getByLabel("新清单名称").fill(uiTodoListName);
|
||||
await page.getByRole("button", { name: "创建" }).click();
|
||||
const uiTodoRow = page.locator(".todo-instance-row", { hasText: uiTodoListName });
|
||||
await uiTodoRow.waitFor({ state: "visible", timeout: 10000 });
|
||||
await uiTodoRow.click();
|
||||
await page.waitForFunction((name) => {
|
||||
const active = document.querySelector('[data-testid="todo-note-page"] .todo-instance-row.active') as HTMLElement | null;
|
||||
return active?.innerText.includes(String(name));
|
||||
}, uiTodoListName, { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="todo-note-tree"]', { timeout: 10000 });
|
||||
await page.getByLabel("新增根任务").fill("UI E2E smoke task");
|
||||
await page.getByRole("button", { name: "新增" }).click();
|
||||
await page.waitForSelector("text=UI E2E smoke task", { timeout: 10000 });
|
||||
const todoNoteText = await page.locator('[data-testid="todo-note-page"]').innerText({ timeout: 5000 });
|
||||
await uiTodoRow.click();
|
||||
await page.waitForFunction((name) => {
|
||||
const active = document.querySelector('[data-testid="todo-note-page"] .todo-instance-row.active') as HTMLElement | null;
|
||||
return active?.innerText.includes(String(name));
|
||||
}, uiTodoListName, { timeout: 10000 });
|
||||
await page.getByRole("button", { name: "删除清单" }).click();
|
||||
await page.waitForFunction((name) => !document.body.innerText.includes(String(name)), uiTodoListName, { timeout: 10000 });
|
||||
await page.getByRole("button", { name: /FindJob/ }).click();
|
||||
await page.waitForSelector('[data-testid="findjob-page"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
@@ -458,6 +549,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const findjobText = await page.locator('[data-testid="findjob-page"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: /Pipeline/ }).click();
|
||||
await page.waitForSelector('[data-testid="pipeline-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="pipeline-react-flow"] .react-flow__node', { timeout: 30000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText;
|
||||
const lower = text.toLowerCase();
|
||||
@@ -468,12 +560,16 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& /组件\s+\d+/.test(text)
|
||||
&& /运行记录\s+[1-9]\d*/.test(text);
|
||||
}, undefined, { timeout: 30000 });
|
||||
const pipelineFlowNodeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__node').count();
|
||||
const pipelineFlowEdgeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__edge').count();
|
||||
const pipelineText = await page.locator('[data-testid="pipeline-page"]').innerText({ timeout: 5000 });
|
||||
const microserviceCatalogTextLower = microserviceCatalogText.toLowerCase();
|
||||
const todoNoteTextLower = todoNoteText.toLowerCase();
|
||||
const findjobTextLower = findjobText.toLowerCase();
|
||||
const pipelineTextLower = pipelineText.toLowerCase();
|
||||
addCheck(checks, "frontend:login-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && bodyText.includes("核心在线"), { screenshotPath });
|
||||
addCheck(checks, "frontend:public-provider-info-visible", publicFrontendReached && bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && rawText.includes('"status": "online"') && rawText.includes(`"providerId": "${config.providerGateway.id}"`), { frontendUrl: urls.frontendUrl, landedUrl, providerId: config.providerGateway.id, rawTextPreview: rawText.slice(0, 400) });
|
||||
addCheck(checks, "frontend:sidebar-collapse", railWidthBefore >= 160 && railWidthCollapsed <= 70, { railWidthBefore, railWidthCollapsed });
|
||||
addCheck(checks, "frontend:mobile-nav-fixed-height", mobileRailMax - mobileRailMin <= 1 && mobileRailMax <= 44, { mobileRailHeights });
|
||||
addCheck(checks, "frontend:mobile-content-top-aligned", mobileContentMetrics.pageTop <= 190 && mobileContentMetrics.emptyTextOffset <= 14, { mobileContentMetrics });
|
||||
addCheck(checks, "frontend:pending-task-drilldown", pendingTaskText.includes("待处理任务") && (pendingTaskText.includes("当前无待处理任务") || (pendingTaskText.includes("Provider") && pendingTaskText.includes("已等待"))), { pendingTaskPreview: pendingTaskText.slice(0, 600) });
|
||||
@@ -485,9 +581,12 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addCheck(checks, "frontend:docker-status-visible", dockerText.toLowerCase().includes("docker desktop 视图") && dockerText.toLowerCase().includes("containers") && dockerText.includes("unidesk_pgdata_10gb") && (dockerText.includes("unidesk-frontend") || dockerText.includes("unidesk-backend-core")), { dockerTextPreview: dockerText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:gateway-version-records-visible", gatewayTextLower.includes("provider gateway 版本") && gatewayText.includes("自动更新记录") && gatewayText.includes(config.providerGateway.id) && gatewayText.includes(`v${providerGatewayPackageVersion()}`) && gatewayText.includes("provider.upgrade"), { gatewayTextPreview: gatewayText.slice(0, 900) });
|
||||
addCheck(checks, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts });
|
||||
addCheck(checks, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogText.includes("D601") && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 1200) });
|
||||
addCheck(checks, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 1400) });
|
||||
addCheck(checks, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) });
|
||||
addCheck(checks, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) });
|
||||
addCheck(checks, "frontend:pipeline-integrated-visible", pipelineTextLower.includes("pipeline v2 工作台".toLowerCase()) && pipelineText.includes("D601") && pipelineText.includes("控制图") && pipelineText.includes("最近运行") && pipelineText.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(pipelineText) && /组件\s+\d+/.test(pipelineText) && /运行记录\s+[1-9]\d*/.test(pipelineText), { pipelineTextPreview: pipelineText.slice(0, 1200) });
|
||||
addCheck(checks, "frontend:pipeline-react-flow-visible", pipelineFlowNodeCount > 0 && pipelineFlowEdgeCount > 0, { pipelineFlowNodeCount, pipelineFlowEdgeCount });
|
||||
addCheck(checks, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors });
|
||||
return { screenshotPath, bodyText, consoleErrors };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user