fix: stabilize Toolbox navigation layout
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from "@testing-library/react";
|
||||
import {
|
||||
TOOLBOX_PREFERENCES_KEY,
|
||||
type ToolboxAppManifest,
|
||||
} from "@add-ideas/toolbox-contract";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AppShell } from "../src/index.js";
|
||||
import { AppShell, ToolboxHeader } from "../src/index.js";
|
||||
|
||||
const currentApp: ToolboxAppManifest = {
|
||||
schemaVersion: 1,
|
||||
@@ -112,16 +118,40 @@ describe("AppShell", () => {
|
||||
expect(
|
||||
document.querySelector("[data-toolbox-context='standalone']"),
|
||||
).toBeInTheDocument();
|
||||
const controls = screen.getByLabelText("Toolbox controls");
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
Array.from(controls.children, (element) =>
|
||||
element.getAttribute("aria-label"),
|
||||
),
|
||||
).toEqual([
|
||||
"Personalize",
|
||||
"Apps",
|
||||
"Source for PDF Workbench on Gitea",
|
||||
"Help",
|
||||
]);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apps" }));
|
||||
expect(
|
||||
screen.getByRole("navigation", { name: "Toolbox applications" }),
|
||||
).toHaveTextContent("Open this app from Toolbox");
|
||||
expect(
|
||||
screen.getByRole("link", { name: /Source for PDF Workbench/u }),
|
||||
).toHaveAttribute("href", "https://git.example.test/pdf-tools");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Help" }));
|
||||
expect(onHelp).toHaveBeenCalledOnce();
|
||||
expect(screen.getByText("Personalize")).toBeInTheDocument();
|
||||
expect(screen.getByText("Apps")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Personalize" }),
|
||||
).toHaveTextContent("Personalize");
|
||||
expect(screen.getByRole("button", { name: "Apps" })).toHaveTextContent(
|
||||
"Apps",
|
||||
);
|
||||
expect(document.querySelector(".toolbox-shell__icon")).toBeNull();
|
||||
expect(screen.getByText("add·ideas Toolbox").closest("a")).toBeNull();
|
||||
expect(document.querySelector(".toolbox-shell__footer")).toContainElement(
|
||||
screen.getByLabelText("Version 1.2.3"),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads same-origin context and creates full-page contextual switch links", async () => {
|
||||
@@ -140,7 +170,13 @@ describe("AppShell", () => {
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
const navigation = await screen.findByRole("navigation", {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector("[data-toolbox-context='connected']"),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apps" }));
|
||||
const navigation = screen.getByRole("navigation", {
|
||||
name: "Toolbox applications",
|
||||
});
|
||||
expect(navigation).toBeInTheDocument();
|
||||
@@ -152,10 +188,14 @@ describe("AppShell", () => {
|
||||
expect(destination.searchParams.get("toolbox")).toBe(
|
||||
"https://tools.example.test/toolbox.catalog.json",
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "add·ideas" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://tools.example.test/",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("link", { name: "add·ideas Toolbox" }),
|
||||
).toHaveAttribute("href", "https://tools.example.test/");
|
||||
expect(
|
||||
screen
|
||||
.getByRole("link", { name: "add·ideas Toolbox" })
|
||||
.querySelector("img"),
|
||||
).toHaveAttribute("src", "https://tools.example.test/favicon.svg");
|
||||
expect(
|
||||
document.querySelector("[data-toolbox-context='connected']"),
|
||||
).toBeInTheDocument();
|
||||
@@ -183,8 +223,9 @@ describe("AppShell", () => {
|
||||
code: "cross-origin",
|
||||
});
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apps" }));
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
screen.getByRole("navigation", { name: "Toolbox applications" }),
|
||||
).toHaveTextContent("Open this app from Toolbox");
|
||||
});
|
||||
|
||||
@@ -236,7 +277,7 @@ describe("AppShell", () => {
|
||||
|
||||
const shell = document.querySelector(".toolbox-shell");
|
||||
expect(shell).toHaveAttribute("data-toolbox-theme", "dark");
|
||||
fireEvent.click(screen.getByText("Personalize"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Light" }));
|
||||
expect(shell).toHaveAttribute("data-toolbox-theme", "light");
|
||||
expect(
|
||||
@@ -244,10 +285,19 @@ describe("AppShell", () => {
|
||||
).toMatchObject({ theme: "light" });
|
||||
});
|
||||
|
||||
it("resolves a relative explicit manifest URL against the deployed page", async () => {
|
||||
it("resolves manifest-relative header links without rendering an app icon", async () => {
|
||||
render(
|
||||
<AppShell
|
||||
app={currentApp}
|
||||
app={{
|
||||
...currentApp,
|
||||
actions: [
|
||||
{
|
||||
id: "source",
|
||||
label: "Source",
|
||||
url: "./source/",
|
||||
},
|
||||
],
|
||||
}}
|
||||
manifestUrl="./toolbox-app.json"
|
||||
contextOptions={{
|
||||
location: "https://tools.example.test/nested/pdf/index.html",
|
||||
@@ -257,9 +307,122 @@ describe("AppShell", () => {
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
expect(document.querySelector(".toolbox-shell__icon")).toHaveAttribute(
|
||||
"src",
|
||||
"https://tools.example.test/nested/pdf/icon.svg",
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Source for PDF Workbench on Gitea" }),
|
||||
).toHaveAttribute("href", "https://tools.example.test/nested/pdf/source/");
|
||||
expect(document.querySelector(".toolbox-shell__icon")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses one accessible controlled popover for Personalize and Apps", () => {
|
||||
const onHelp = vi.fn();
|
||||
render(
|
||||
<ToolboxHeader
|
||||
title="PDF Workbench"
|
||||
subtitle="Local PDF tools"
|
||||
theme="system"
|
||||
personalizeContent={<a href="#preferences">Full preferences</a>}
|
||||
helpAction={{ onClick: onHelp }}
|
||||
sourceHref="https://git.example.test/pdf-tools"
|
||||
apps={[
|
||||
{
|
||||
id: "xslt",
|
||||
name: "XSLT Workbench",
|
||||
href: "#xslt",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const personalize = screen.getByRole("button", { name: "Personalize" });
|
||||
const apps = screen.getByRole("button", { name: "Apps" });
|
||||
expect(
|
||||
screen.getByRole("group", { name: "Toolbox controls" }),
|
||||
).toBeInTheDocument();
|
||||
expect(personalize).toHaveAttribute("aria-label", "Personalize");
|
||||
expect(apps).toHaveAttribute("aria-label", "Apps");
|
||||
expect(apps).not.toHaveAttribute("aria-haspopup");
|
||||
|
||||
fireEvent.click(personalize);
|
||||
expect(
|
||||
screen.getByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).toHaveTextContent("Full preferences");
|
||||
expect(personalize).toHaveAttribute("aria-expanded", "true");
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Full preferences" }),
|
||||
).toHaveFocus();
|
||||
|
||||
fireEvent.click(apps);
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).not.toBeInTheDocument();
|
||||
const navigation = screen.getByRole("navigation", {
|
||||
name: "Toolbox applications",
|
||||
});
|
||||
expect(navigation).toBeInTheDocument();
|
||||
expect(
|
||||
within(navigation).getByRole("link", { name: "XSLT Workbench" }),
|
||||
).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(apps).toHaveFocus();
|
||||
|
||||
fireEvent.click(personalize);
|
||||
fireEvent.pointerDown(document.body);
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(apps);
|
||||
fireEvent.click(
|
||||
within(
|
||||
screen.getByRole("navigation", { name: "Toolbox applications" }),
|
||||
).getByRole("link", { name: "XSLT Workbench" }),
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the legacy onPersonalize callback when no popover content exists", () => {
|
||||
const onPersonalize = vi.fn();
|
||||
const onThemeChange = vi.fn();
|
||||
render(
|
||||
<ToolboxHeader
|
||||
title="PDF Workbench"
|
||||
theme="system"
|
||||
onPersonalize={onPersonalize}
|
||||
onThemeChange={onThemeChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
|
||||
expect(onPersonalize).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes Personalize if its content becomes unavailable", () => {
|
||||
const { rerender } = render(
|
||||
<ToolboxHeader
|
||||
title="PDF Workbench"
|
||||
theme="system"
|
||||
onThemeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
|
||||
expect(
|
||||
screen.getByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
rerender(<ToolboxHeader title="PDF Workbench" theme="system" />);
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Personalize" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user