119 lines
6.7 KiB
JavaScript
119 lines
6.7 KiB
JavaScript
import { chromium } from "../tools/playwright/node_modules/playwright/index.mjs";
|
|
import assert from "node:assert/strict";
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, serviceWorkers: "block" });
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(8000);
|
|
await page.addInitScript(() => localStorage.setItem("lazyboy.locale", "zh-Hant"));
|
|
const errors = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
const idle = { state: "idle", running: false, queued: false, active_task_ids: [], observed_at_ms: Date.now() };
|
|
const agents = [{ id: "alpha", name: "Alpha", activity: idle }, { id: "beta", name: "Beta", activity: idle }];
|
|
let expired = false;
|
|
let messageMode = "fail";
|
|
let hold = false;
|
|
let release;
|
|
let sent = 0;
|
|
const protectedRequests = [];
|
|
await page.route("**/api/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
const cookie = request.headers().cookie || "";
|
|
const authenticated = cookie.includes("lazyboy_session=fixture") && !expired;
|
|
if (path === "/api/auth") {
|
|
if (request.method() === "POST") {
|
|
if (request.headers().authorization !== "Bearer correct-token") return route.fulfill({ status: 401, body: "" });
|
|
expired = false;
|
|
return route.fulfill({ json: { authenticated: true }, headers: { "Set-Cookie": "lazyboy_session=fixture; Path=/; HttpOnly; SameSite=Strict" } });
|
|
}
|
|
return route.fulfill({ json: { required: true, authenticated } });
|
|
}
|
|
protectedRequests.push({ path, cookie, authorization: request.headers().authorization });
|
|
if (!authenticated) return route.fulfill({ status: 401, json: { error: "unauthorized" } });
|
|
if (path === "/api/agents") return route.fulfill({ json: { agents } });
|
|
if (path.endsWith("/activity")) return route.fulfill({ json: idle });
|
|
if (path.endsWith("/events")) return route.fulfill({ contentType: "text/event-stream", body: 'data: {"events":[]}\n\n' });
|
|
if (path.endsWith("/computer")) return route.fulfill({ json: { ready: false, state: "error", error: "fixture" } });
|
|
if (path.endsWith("/messages")) {
|
|
sent += 1;
|
|
if (hold) await new Promise((resolve) => { release = resolve; });
|
|
return messageMode === "fail"
|
|
? route.fulfill({ status: 503, json: { error: "Alpha request failed" } })
|
|
: route.fulfill({ json: { queued: "fixture" } });
|
|
}
|
|
const agent = agents.find((item) => path === `/api/agents/${item.id}`);
|
|
if (agent) return route.fulfill({ json: { ...agent, transcript: [{ role: "assistant", content: `Hello ${agent.name}` }] } });
|
|
return route.fulfill({ json: {} });
|
|
});
|
|
|
|
await page.goto(process.env.LAZYBOY_TEST_UI_URL || "http://127.0.0.1:5173");
|
|
await page.getByLabel("存取憑證").waitFor();
|
|
assert.equal(protectedRequests.length, 0);
|
|
await page.getByLabel("存取憑證").fill("wrong-token");
|
|
await page.getByRole("button", { name: "登入", exact: true }).click();
|
|
await page.getByRole("alert").getByText("憑證不正確,請再試一次。").waitFor();
|
|
await page.getByLabel("存取憑證").fill("correct-token");
|
|
await page.getByRole("button", { name: "登入", exact: true }).click();
|
|
await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor();
|
|
await page.waitForRequest((request) => request.url().endsWith("/events") && request.headers().cookie?.includes("lazyboy_session=fixture"));
|
|
assert.ok(protectedRequests.every((request) => request.cookie.includes("lazyboy_session=fixture") && !request.authorization));
|
|
const storage = await page.evaluate(() => ({ local: JSON.stringify(localStorage), session: JSON.stringify(sessionStorage), cookie: document.cookie }));
|
|
assert.ok(!JSON.stringify(storage).includes("correct-token"));
|
|
assert.ok(!storage.cookie.includes("lazyboy_session"));
|
|
console.log("PASS token login gates workspace; APIs and event streams use an HttpOnly cookie");
|
|
|
|
const composer = page.locator(".composer textarea");
|
|
await composer.fill("Unsent message");
|
|
await page.locator(".composer button.send").click();
|
|
await page.getByRole("alert").getByText("Alpha request failed").waitFor();
|
|
assert.equal(await composer.inputValue(), "Unsent message");
|
|
assert.equal(await page.locator(".messages").getByText("Unsent message", { exact: true }).count(), 0);
|
|
console.log("PASS failed send retains draft and removes the optimistic message");
|
|
|
|
hold = true;
|
|
await composer.fill("Second Alpha request");
|
|
await page.locator(".composer button.send").click();
|
|
await page.waitForResponse((response) => response.url().endsWith("/activity"));
|
|
assert.equal(sent, 2);
|
|
assert.equal(await composer.inputValue(), "Second Alpha request");
|
|
await page.locator(".bot-row strong").getByText("Beta", { exact: true }).click();
|
|
await page.locator(".messages").getByText("Hello Beta", { exact: true }).waitFor();
|
|
assert.equal(await composer.inputValue(), "");
|
|
await composer.fill("Beta draft");
|
|
const failed = page.waitForResponse((response) => response.url().endsWith("/messages") && response.status() === 503);
|
|
release();
|
|
await failed;
|
|
await page.waitForTimeout(100);
|
|
assert.equal(await composer.inputValue(), "Beta draft");
|
|
assert.equal(await page.getByRole("alert").getByText("Alpha request failed").count(), 0);
|
|
await page.locator(".bot-row strong").getByText("Alpha", { exact: true }).click();
|
|
await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor();
|
|
assert.equal(await composer.inputValue(), "Second Alpha request");
|
|
console.log("PASS delayed failure stays with the originating agent; each agent retains its draft");
|
|
|
|
messageMode = "success";
|
|
await page.locator(".composer button.send").click();
|
|
await page.waitForResponse((response) => response.url().endsWith("/activity"));
|
|
await composer.fill("Edited while sending");
|
|
const accepted = page.waitForResponse((response) => response.url().endsWith("/messages") && response.status() === 200);
|
|
release();
|
|
await accepted;
|
|
await page.waitForTimeout(100);
|
|
assert.equal(await composer.inputValue(), "Edited while sending");
|
|
hold = false;
|
|
await page.locator(".composer button.send").click();
|
|
await page.waitForFunction(() => document.querySelector(".composer textarea")?.value === "");
|
|
console.log("PASS accepted sends clear only the submitted draft, preserving subsequent edits");
|
|
|
|
await page.reload();
|
|
await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor();
|
|
expired = true;
|
|
await page.getByLabel("存取憑證").waitFor();
|
|
assert.deepEqual(errors, []);
|
|
console.log("PASS session survives reload; expired credentials return to login");
|
|
} finally {
|
|
await browser.close();
|
|
}
|