Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions packages/webui/test/browser/browser-workspace.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ const openPanel = async (page) => {
.getByRole("button", { name: "浏览器" });
await browserButton.click();
await expect(page.getByTestId("workspace-panel")).toBeVisible();
await expect(page.getByTestId("workspace-launcher")).toBeVisible();
// The panel's browser half shows the pages the runtime published, and the
// launcher is the empty case: the control opens a page when the runtime has
// published one. So the launcher is not what opening the panel guarantees —
// `openLauncher` below is what asks for it.
};

/** The launcher's address box, opened the way a person opens it. */
Expand All @@ -43,13 +46,16 @@ const openPage = async (page, url) => {
await page.getByTestId("workspace-launcher-url-submit").click();
};

test("the browser control shows its globe label and opens a new tab page by default", async ({ page }) => {
test("the browser control shows its browser glyph and opens a new tab page by default", async ({ page }) => {
await configureFixture(page, () => undefined);
await openApp(page);

const browserButton = page.getByTestId("workspace-panel-controls").getByRole("button", { name: "浏览器" });
await expect(browserButton).toHaveAttribute("title", "浏览器");
await expect(browserButton.locator("svg circle")).toHaveCount(1);
// The control's glyph is the tabler browser icon. It used to be a globe
// drawn as an SVG circle, and the library draws this one as paths, so the
// assertion names the icon rather than the shape it happens to be made of.
await expect(browserButton.locator("svg.tabler-icon-browser")).toHaveCount(1);
await browserButton.click();

await expect(page.getByTestId("workspace-panel")).toHaveAttribute("data-active-tab", "launcher");
Expand Down Expand Up @@ -135,6 +141,9 @@ test("the browser page fills the workspace body and the workspace edge resizes h
expect(brandBox.height).toBe(80);
await page.getByTestId("workspace-launcher-url").fill("https://example.test/");
await page.getByTestId("workspace-launcher-url-submit").click();
// The panel swaps the launcher for the page, and the swap re-mounts the
// panel: a gesture started before it finished is lost with the old tree.
await expect(page.getByTestId("browser-toolbar")).toBeVisible();

const stage = page.locator(".webui-browser-stage");
const frame = page.getByTestId("browser-panel-frame");
Expand All @@ -152,8 +161,13 @@ test("the browser page fills the workspace body and the workspace edge resizes h
await page.mouse.down();
await page.mouse.move(before.x - 56, before.y + before.height / 2);
await page.mouse.up();
// The panel's width is a controlled style, so the drag lands in state and
// the layout follows when React flushes it — the box is polled rather than
// read once, the same way the keyboard nudge below is waited for.
await expect
.poll(async () => (await panel.boundingBox()).width)
.toBeGreaterThan(before.width + 40);
const after = await panel.boundingBox();
expect(after.width).toBeGreaterThan(before.width + 40);
await expect(handle).toBeVisible();
await handle.focus();
await page.keyboard.press("ArrowLeft");
Expand Down
5 changes: 4 additions & 1 deletion packages/webui/test/unit/webui-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ describe("WebUI shell", () => {
const controls = renderToStaticMarkup(createElement(WebuiWorkspacePanelControls, { browserPanelOpen: false, progressPanelOpen: true, onOpenBrowser: () => undefined, onToggleProgressPanel: () => undefined }));
expect(controls).toContain('aria-label="浏览器"');
expect(controls).toContain('title="浏览器"');
expect(controls).toContain('<circle cx="8" cy="8" r="5.2"');
// The control's glyph is the tabler browser icon. It used to be a globe
// drawn by hand, so the assertion names the icon rather than the shape it
// happens to be made of.
expect(controls).toContain("tabler-icon-browser");
expect(controls).toContain('aria-label="环境信息与进度"');
});

Expand Down
68 changes: 61 additions & 7 deletions packages/webui/test/unit/webui-w0-css-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,52 @@ function declarationsFor(selector: string, rules: CssRule[] = shellRules): strin
.join("\n");
}

/** The selectors a rule targets: a rule may group several of them. */
function targetsOf(rule: CssRule): readonly string[] {
return rule.selector.split(",").map((part) => part.trim());
}

/** Whether a rule targets the selector, naming it alone or in a group. */
function targets(rule: CssRule, selector: string): boolean {
return targetsOf(rule).includes(selector);
}

/**
* The value the cascade gives a property for a selector.
*
* A rule may group several selectors, and the declaration a reader cares
* about may live in the grouped rule rather than in a rule that names the
* selector on its own — a stylesheet that collapses three scroll areas into
* one rule keeps the behaviour and loses the single-selector lookup. So the
* lookup considers every rule that targets the selector, and among those the
* last one that actually declares the property.
*/
function winningDeclaration(
selector: string,
property: string,
rules: CssRule[] = shellRules,
): string | undefined {
const declaring = rules.filter(
(rule) =>
targets(rule, selector) &&
isCascading(rule) &&
declaration(rule.body, property) !== undefined,
);
if (declaring.length === 0) return undefined;
return declaration(declaring[declaring.length - 1]!.body, property);
}

/** Every declaration that reaches a selector, grouped rules included. */
function declarationsForTargeting(
selector: string,
rules: CssRule[] = shellRules,
): string {
return rules
.filter((rule) => targets(rule, selector) && isCascading(rule))
.map((rule) => rule.body)
.join("\n");
}

beforeAll(() => {
indexCss = readFileSync(path.join(stylesDir, "index.css"), "utf8");
// The rules under test are the shell's own population: the feature
Expand Down Expand Up @@ -570,12 +616,12 @@ describe("W0 · structural declarations W5 must preserve", () => {
);
expect(declaration(progressScroll.body, "overflow-y")).toBe("auto");
expect(declaration(progressScroll.body, "overflow-x")).toBe("hidden");
expect(declaration(winning(".webui-progress-scroll").body, "scrollbar-width")).toBe("none");
expect(declaration(winning(".webui-subagents-scroll").body, "scrollbar-width")).toBe("none");
expect(declaration(winning(".webui-progress-scroll").body, "scrollbar-gutter")).toBe("auto");
expect(declaration(winning(".webui-subagents-scroll").body, "scrollbar-gutter")).toBe("auto");
expect(declarationsFor(".webui-progress-scroll::-webkit-scrollbar")).toContain("display: none");
expect(declarationsFor(".webui-subagents-scroll::-webkit-scrollbar")).toContain("display: none");
expect(winningDeclaration(".webui-progress-scroll", "scrollbar-width")).toBe("none");
expect(winningDeclaration(".webui-subagents-scroll", "scrollbar-width")).toBe("none");
expect(winningDeclaration(".webui-progress-scroll", "scrollbar-gutter")).toBe("auto");
expect(winningDeclaration(".webui-subagents-scroll", "scrollbar-gutter")).toBe("auto");
expect(declarationsForTargeting(".webui-progress-scroll::-webkit-scrollbar")).toContain("display: none");
expect(declarationsForTargeting(".webui-subagents-scroll::-webkit-scrollbar")).toContain("display: none");
expect(
declaration(winning(".webui-progress-section.is-collapsed").body, "flex-basis"),
).toBe("28px");
Expand Down Expand Up @@ -964,6 +1010,14 @@ describe("W0 · stacking order", () => {
// pinned headers (1 and 2) because the browser panel is an in-flow
// region of the shell rather than an overlay: nothing it draws may
// cover the goal overlay at 20 or the workspace drawer at 40.
// 5 is `.webui-workspace-panel-resize-handle`, the eight-pixel strip on
// the panel's left edge that drags its width. It is the panel's own
// child, so it needs a layer of its own to sit above the panel's
// content. It stays in the low band with the pinned headers (1 and 2)
// and the browser notice (3): the panel is an in-flow region of the
// shell, so nothing it draws may cover the goal overlay at 20 or the
// workspace drawer at 40. It is an edge affordance rather than an
// overlay — eight pixels wide, transparent, and on the panel's border.
// 40 is `.webui-workspace-panel` inside `@media (max-width: 1080px)`.
// Above that breakpoint the panel is an in-flow flex sibling and needs no
// layer; below it the panel is pulled out of flow into a right-docked
Expand Down Expand Up @@ -1007,7 +1061,7 @@ describe("W0 · stacking order", () => {
// 4 is gone: it was the workspace panel's add-menu anchor, and the add
// menu became the launcher's tiles. The launcher is a tab's content
// rather than an overlay, so it needs no layer of its own.
1, 2, 3, 20, 40, 45, 50, 60, 70, 100, 110, 111, 120, 121, 130, 140, 1400,
1, 2, 3, 5, 20, 40, 45, 50, 60, 70, 100, 110, 111, 120, 121, 130, 140, 1400,
]);
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/webui/test/unit/workspace-tab-strip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ describe("the workspace panel's tab strip", () => {

expect(markup).toContain('data-tab-kind="launcher"');
expect(markup).toContain("webui-workspace-web-glyph");
expect(markup).toContain("<circle");
// The globe is the tabler world icon now; it used to be drawn by hand, so
// the assertion names the icon rather than the shape it is made of.
expect(markup).toContain("tabler-icon-world");
});

it("renders tool tabs and web tabs in one row", () => {
Expand Down
Loading