Skip to content

Commit 0717483

Browse files
committed
test(pdf-server): fix get_viewer_state e2e race + assertions
readLastToolResult clicked .last() before the interact result panel existed (callInteract doesn't block), so it expanded the display_pdf panel instead. Wait for the expected panel count first. Also: basic-host renders the full CallToolResult JSON, with the state double-escaped inside content[0].text. Parse instead of regex-matching. playwright.config.ts: honor PW_CHANNEL env to use system Chrome locally when the bundled chromium_headless_shell is broken.
1 parent 1836155 commit 0717483

2 files changed

Lines changed: 44 additions & 21 deletions

File tree

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default defineConfig({
3030
...devices["Desktop Chrome"],
3131
// Use default Chromium everywhere for consistent screenshot rendering
3232
// Run `npm run test:e2e:docker` locally for CI-identical results
33+
...(process.env.PW_CHANNEL ? { channel: process.env.PW_CHANNEL } : {}),
3334
},
3435
},
3536
],

tests/e2e/pdf-annotations.spec.ts

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -317,17 +317,32 @@ test.describe("PDF Server - Annotations", () => {
317317

318318
/**
319319
* Read the most recent interact result text from the basic-host UI.
320-
* Expands the latest "📤 Tool Result" panel and returns the <pre> text.
320+
* Waits for the result-panel count to reach `expectedCount` first —
321+
* `callInteract` doesn't block, so `.last()` would otherwise race to the
322+
* previous (display_pdf) panel.
321323
*/
322-
async function readLastToolResult(page: Page): Promise<string> {
323-
const panel = page.locator('text="📤 Tool Result"').last();
324-
await expect(panel).toBeVisible({ timeout: 30000 });
325-
await panel.click();
324+
async function readLastToolResult(
325+
page: Page,
326+
expectedCount: number,
327+
): Promise<string> {
328+
const panels = page.locator('text="📤 Tool Result"');
329+
await expect(panels).toHaveCount(expectedCount, { timeout: 30000 });
330+
await panels.last().click();
326331
const pre = page.locator("pre").last();
327332
await expect(pre).toBeVisible({ timeout: 5000 });
328333
return (await pre.textContent()) ?? "";
329334
}
330335

336+
/** Unwrap basic-host's `CallToolResult` JSON to the first text block. */
337+
function unwrapTextResult(raw: string): string {
338+
const parsed = JSON.parse(raw) as {
339+
content?: { type: string; text?: string }[];
340+
};
341+
const block = parsed.content?.find((c) => c.type === "text");
342+
if (!block?.text) throw new Error(`No text block in: ${raw.slice(0, 200)}`);
343+
return block.text;
344+
}
345+
331346
test.describe("PDF Server - get_viewer_state", () => {
332347
test("returns page/zoom/mode and selection:null when nothing is selected", async ({
333348
page,
@@ -338,16 +353,15 @@ test.describe("PDF Server - get_viewer_state", () => {
338353
const viewUUID = await extractViewUUID(page);
339354

340355
await callInteract(page, { viewUUID, action: "get_viewer_state" });
341-
const result = await readLastToolResult(page);
342-
343-
// Basic-host renders text content blocks as JSON-ish; the viewer's reply
344-
// is a JSON object — assert key fields without being brittle on
345-
// surrounding chrome.
346-
expect(result).toMatch(/"currentPage"\s*:\s*1/);
347-
expect(result).toMatch(/"pageCount"\s*:\s*\d+/);
348-
expect(result).toMatch(/"zoom"\s*:\s*\d+/);
349-
expect(result).toMatch(/"displayMode"\s*:\s*"inline"/);
350-
expect(result).toMatch(/"selection"\s*:\s*null/);
356+
const raw = await readLastToolResult(page, 2);
357+
const state = JSON.parse(unwrapTextResult(raw));
358+
359+
expect(state.currentPage).toBe(1);
360+
expect(state.pageCount).toBeGreaterThan(1);
361+
expect(typeof state.zoom).toBe("number");
362+
expect(state.displayMode).toBe("inline");
363+
expect(state.selection).toBeNull();
364+
expect(Array.isArray(state.selectedAnnotationIds)).toBe(true);
351365
});
352366

353367
test("returns selected text and bounding rect when text-layer text is selected", async ({
@@ -374,11 +388,19 @@ test.describe("PDF Server - get_viewer_state", () => {
374388
expect(selectedText.length).toBeGreaterThan(0);
375389

376390
await callInteract(page, { viewUUID, action: "get_viewer_state" });
377-
const result = await readLastToolResult(page);
378-
379-
expect(result).toMatch(/"selection"\s*:\s*\{/);
380-
expect(result).toContain(JSON.stringify(selectedText).slice(1, -1));
381-
expect(result).toMatch(/"boundingRect"\s*:\s*\{/);
382-
expect(result).toMatch(/"currentPage"\s*:\s*1/);
391+
const raw = await readLastToolResult(page, 2);
392+
const state = JSON.parse(unwrapTextResult(raw));
393+
394+
expect(state.currentPage).toBe(1);
395+
expect(state.selection).not.toBeNull();
396+
expect(state.selection.text).toContain(selectedText);
397+
expect(state.selection.boundingRect).toEqual(
398+
expect.objectContaining({
399+
x: expect.any(Number),
400+
y: expect.any(Number),
401+
width: expect.any(Number),
402+
height: expect.any(Number),
403+
}),
404+
);
383405
});
384406
});

0 commit comments

Comments
 (0)