import { type Locator, type Page, expect, test } from "@playwright/test";
import { mockLangGraphAPI } from "./utils/mock-api";
const ARTIFACT_PATH = "/artifact-fixtures/report.html";
const THREAD_ID = "00000000-0000-0000-0000-000000003125";
function writeFileMessages() {
return [
{
type: "human",
id: "msg-human-artifact",
content: [{ type: "text", text: "Create a report artifact" }],
},
{
type: "ai",
id: "msg-ai-write-artifact",
content: "",
tool_calls: [
{
id: "write-file-artifact",
name: "write_file",
args: {
description: "Writing report artifact",
path: ARTIFACT_PATH,
content:
"
Report draft
",
},
},
],
},
{
type: "tool",
id: "msg-tool-write-artifact",
name: "write_file",
tool_call_id: "write-file-artifact",
content: "OK",
},
];
}
async function panelWidth(panel: Locator): Promise {
const box = await panel.boundingBox();
return box?.width ?? 0;
}
/** Drag the divider left by `distance` px, widening the right panel. */
async function widenPanel(handle: Locator, distance: number): Promise {
// hover() waits for a stable bounding box: the open animation moves the
// 1px-wide divider, so coordinates read any earlier miss it entirely.
await handle.hover();
await expect(handle).toHaveAttribute("data-separator", "hover");
const box = await handle.boundingBox();
expect(box).not.toBeNull();
const y = box!.y + box!.height / 2;
const x = box!.x + box!.width / 2;
const mouse = handle.page().mouse;
await mouse.down();
await expect(handle).toHaveAttribute("data-separator", "active");
await mouse.move(x - distance, y, { steps: 10 });
await mouse.up();
}
async function openArtifact(page: Page): Promise {
await expect(page.getByText(ARTIFACT_PATH)).toBeVisible({ timeout: 15_000 });
await page.getByText(ARTIFACT_PATH).click();
}
test.describe("Artifacts panel resize", () => {
test.beforeEach(async ({ page }) => {
mockLangGraphAPI(page, {
threads: [
{
thread_id: THREAD_ID,
title: "Artifact panel resize",
messages: writeFileMessages(),
},
],
});
await page.goto(`/workspace/chats/${THREAD_ID}`);
});
test("the divider resizes the artifacts panel", async ({ page }) => {
await openArtifact(page);
const artifactsPanel = page.locator("#artifacts");
await expect(artifactsPanel).toBeVisible();
const handle = page.locator('[data-slot="resizable-handle"]');
await expect(handle).toBeVisible();
await handle.hover();
const widthBefore = await panelWidth(artifactsPanel);
expect(widthBefore).toBeGreaterThan(0);
await widenPanel(handle, 200);
await expect
.poll(async () => panelWidth(artifactsPanel))
.toBeGreaterThan(widthBefore + 100);
});
test("a dragged width is kept when the panel is reopened", async ({
page,
}) => {
await openArtifact(page);
const artifactsPanel = page.locator("#artifacts");
await expect(artifactsPanel).toBeVisible();
const handle = page.locator('[data-slot="resizable-handle"]');
await handle.hover();
const widthBefore = await panelWidth(artifactsPanel);
await widenPanel(handle, 200);
await expect
.poll(async () => panelWidth(artifactsPanel))
.toBeGreaterThan(widthBefore + 100);
const widthAfterDrag = await panelWidth(artifactsPanel);
await artifactsPanel
.getByRole("button", { name: /close/i })
.first()
.click();
await expect(artifactsPanel).toBeHidden();
await openArtifact(page);
await expect(artifactsPanel).toBeVisible();
await expect
.poll(async () => panelWidth(artifactsPanel))
.toBeGreaterThan(widthAfterDrag - 20);
});
test("opening animates the width, dragging does not", async ({ page }) => {
await expect(page.getByText(ARTIFACT_PATH)).toBeVisible({
timeout: 15_000,
});
// Listen for the transition instead of sampling widths: `transitionrun`
// fires when the transition is created, so a loaded machine dropping
// frames cannot turn a real animation into a missed one.
await page.evaluate(() => {
const store = window as unknown as { __transitions?: string[] };
store.__transitions = [];
document.addEventListener(
"transitionrun",
(event) => {
store.__transitions?.push(event.propertyName);
},
true,
);
});
await page.getByText(ARTIFACT_PATH).click();
await expect
.poll(async () =>
page.evaluate(
() =>
(window as unknown as { __transitions: string[] }).__transitions,
),
)
.toContain("flex-grow");
const artifactsPanel = page.locator("#artifacts");
const handle = page.locator('[data-slot="resizable-handle"]');
await expect(artifactsPanel).toBeVisible();
await handle.hover();
const transitionDuringDrag = await (async () => {
const box = await handle.boundingBox();
const mouse = page.mouse;
await mouse.down();
await mouse.move(box!.x - 120, box!.y + box!.height / 2, { steps: 5 });
const transition = await page.evaluate(() => {
// `[data-panel]` is the element the library sizes; the child that
// `className` lands on is not the flex item.
const panel = document
.querySelector("#artifacts")
?.closest("[data-panel]");
return panel
? window.getComputedStyle(panel).transitionProperty
: "missing";
});
await mouse.up();
return transition;
})();
expect(transitionDuringDrag).not.toContain("flex-grow");
});
});