deer-flow/frontend/tests/e2e/ui-polish-mobile.spec.ts
Ryker_Feng 7a985f441c
Fix(frontend): Fix mobile workspace and accessibility blockers (#3740)
* fix(frontend): address mobile workspace polish blockers

* fix(frontend): prevent mobile landing overflow

* fix(frontend): keep landing sections within mobile viewport

Section titles used a fixed text-5xl with no word breaking, and the
<section> flex items had default min-width:auto, so wider Linux font
metrics in CI pushed content past the viewport (scrollWidth 345 > 320).
Make titles/subtitles responsive with break-words, constrain section
width with min-w-0, and add an overflow-x-clip guard on the page root.

* fix(frontend): address review feedback on mobile landing PR

- add hamburger Sheet nav below sm: so mobile users keep docs/blog access
- switch useIsMobile to useSyncExternalStore to avoid hydration swap flash
- memoize artifactContent so it isn't rebuilt on every streamed token
- use slot-based key in HeroWordRotate; drop flex-wrap so SuperAgent never orphans at 320px
- delete unused word-rotate.tsx dead code
- move section gutter to <main> for a uniform mobile padding contract
- harden e2e: per-viewport overflow tests, locale-stable artifact selector, focus-ring asserts light vs dark differ

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-06 07:15:35 +08:00

79 lines
2.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { MOCK_THREAD_ID, mockLangGraphAPI } from "./utils/mock-api";
test.describe("UI polish mobile regressions", () => {
test("workspace exposes mobile sidebar navigation from the chat header", async ({
page,
}) => {
await page.setViewportSize({ width: 375, height: 812 });
mockLangGraphAPI(page);
await page.goto("/workspace/chats/new");
await page.getByRole("button", { name: /toggle sidebar/i }).click();
await expect(page.getByRole("link", { name: /new chat/i })).toBeVisible();
await expect(page.getByRole("link", { name: /agents/i })).toBeVisible();
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeLessThanOrEqual(375);
});
test("mobile artifacts open in a drawer without horizontal overflow", async ({
page,
}) => {
await page.setViewportSize({ width: 375, height: 812 });
mockLangGraphAPI(page, {
threads: [
{
thread_id: MOCK_THREAD_ID,
title: "Thread with artifact",
artifacts: ["reports/mobile-summary.md"],
},
],
});
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
await page.getByTestId("artifact-trigger").click();
await expect(
page.getByRole("dialog", { name: /artifacts/i }),
).toBeVisible();
await expect(page.getByText("mobile-summary.md")).toBeVisible();
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeLessThanOrEqual(375);
});
test("global focus ring tokens are visible in light and dark themes", async ({
page,
}) => {
mockLangGraphAPI(page);
await page.goto("/workspace/chats/new");
const readRing = () =>
page.evaluate(() =>
getComputedStyle(document.documentElement)
.getPropertyValue("--ring")
.trim(),
);
await page.evaluate(() =>
document.documentElement.classList.remove("dark"),
);
const lightRing = await readRing();
expect(lightRing).not.toBe("transparent");
expect(lightRing).not.toBe("");
await page.evaluate(() => document.documentElement.classList.add("dark"));
const darkRing = await readRing();
expect(darkRing).not.toBe("transparent");
expect(darkRing).not.toBe("");
// The two themes must resolve to different ring tokens, otherwise the test
// would pass trivially if <html> were stuck in one mode.
expect(darkRing).not.toBe(lightRing);
});
});