feat: add toolbox contract shell and testkit
This commit is contained in:
202
packages/shell-react/test/AppShell.test.tsx
Normal file
202
packages/shell-react/test/AppShell.test.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import type { ToolboxAppManifest } from "@add-ideas/toolbox-contract";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AppShell } from "../src/index.js";
|
||||
|
||||
const currentApp: ToolboxAppManifest = {
|
||||
schemaVersion: 1,
|
||||
id: "de.add-ideas.pdf-tools",
|
||||
name: "PDF Workbench",
|
||||
version: "1.2.3",
|
||||
description: "Local PDF tools",
|
||||
entry: "./index.html",
|
||||
icon: "./icon.svg",
|
||||
categories: ["documents", "pdf"],
|
||||
tags: ["merge"],
|
||||
integration: {
|
||||
contextVersion: 1,
|
||||
launchModes: ["navigate", "new-tab"],
|
||||
embedding: "unsupported",
|
||||
},
|
||||
requirements: {
|
||||
secureContext: true,
|
||||
workers: true,
|
||||
indexedDb: true,
|
||||
crossOriginIsolated: false,
|
||||
},
|
||||
privacy: {
|
||||
processing: "local",
|
||||
fileUploads: false,
|
||||
telemetry: false,
|
||||
},
|
||||
source: {
|
||||
repository: "https://git.example.test/pdf-tools",
|
||||
license: "AGPL-3.0-only",
|
||||
},
|
||||
};
|
||||
|
||||
const otherApp: ToolboxAppManifest = {
|
||||
...currentApp,
|
||||
id: "de.add-ideas.xslt-tools",
|
||||
name: "XSLT Workbench",
|
||||
entry: "./index.html",
|
||||
source: {
|
||||
repository: "https://git.example.test/xslt-tools",
|
||||
license: "AGPL-3.0-only",
|
||||
},
|
||||
};
|
||||
|
||||
const catalog = {
|
||||
schemaVersion: 1,
|
||||
id: "de.add-ideas.toolbox",
|
||||
name: "Toolbox",
|
||||
home: "./",
|
||||
theme: { mode: "system", brand: "add·ideas" },
|
||||
apps: [
|
||||
{ manifest: "./pdf/toolbox-app.json", enabled: true },
|
||||
{ manifest: "./xslt/toolbox-app.json", enabled: true },
|
||||
],
|
||||
};
|
||||
|
||||
const json = (value: unknown, status = 200): Response =>
|
||||
new Response(JSON.stringify(value), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
function catalogFetch() {
|
||||
return vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(input instanceof Request ? input.url : input);
|
||||
if (url.pathname === "/toolbox.catalog.json") return json(catalog);
|
||||
if (url.pathname === "/pdf/toolbox-app.json") return json(currentApp);
|
||||
if (url.pathname === "/xslt/toolbox-app.json") return json(otherApp);
|
||||
return json({}, 404);
|
||||
});
|
||||
}
|
||||
|
||||
describe("AppShell", () => {
|
||||
it("renders identity, privacy, version, actions, and content standalone", async () => {
|
||||
const fetch = vi.fn();
|
||||
render(
|
||||
<AppShell
|
||||
app={currentApp}
|
||||
appActions={<a href="/help">Help</a>}
|
||||
contextOptions={{
|
||||
location: "https://tools.example.test/pdf/",
|
||||
fetch,
|
||||
}}
|
||||
>
|
||||
<p>Application content</p>
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: "PDF Workbench" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Version 1.2.3")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Local processing/u)).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Help" })).toHaveAttribute(
|
||||
"href",
|
||||
"/help",
|
||||
);
|
||||
expect(screen.getByText("Application content")).toBeInTheDocument();
|
||||
await waitFor(() => expect(fetch).not.toHaveBeenCalled());
|
||||
expect(
|
||||
document.querySelector("[data-toolbox-context='standalone']"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("loads same-origin context and creates full-page contextual switch links", async () => {
|
||||
const fetch = catalogFetch();
|
||||
render(
|
||||
<AppShell
|
||||
app={currentApp}
|
||||
manifestUrl="https://tools.example.test/pdf/toolbox-app.json"
|
||||
contextOptions={{
|
||||
location:
|
||||
"https://tools.example.test/pdf/?toolbox=/toolbox.catalog.json",
|
||||
fetch,
|
||||
}}
|
||||
>
|
||||
PDF
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
const navigation = await screen.findByRole("navigation", {
|
||||
name: "Toolbox applications",
|
||||
});
|
||||
expect(navigation).toBeInTheDocument();
|
||||
const current = screen.getByRole("link", { name: "PDF Workbench" });
|
||||
expect(current).toHaveAttribute("aria-current", "page");
|
||||
const other = screen.getByRole("link", { name: "XSLT Workbench" });
|
||||
const destination = new URL(other.getAttribute("href") as string);
|
||||
expect(destination.pathname).toBe("/xslt/index.html");
|
||||
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(
|
||||
document.querySelector("[data-toolbox-context='connected']"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("rejects cross-origin context and remains standalone", async () => {
|
||||
const fetch = vi.fn();
|
||||
const onContextError = vi.fn();
|
||||
render(
|
||||
<AppShell
|
||||
app={currentApp}
|
||||
contextOptions={{
|
||||
location:
|
||||
"https://tools.example.test/pdf/?toolbox=https://evil.example/catalog.json",
|
||||
fetch,
|
||||
}}
|
||||
onContextError={onContextError}
|
||||
>
|
||||
PDF
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(onContextError).toHaveBeenCalledOnce());
|
||||
expect(onContextError.mock.calls[0]?.[0]).toMatchObject({
|
||||
code: "cross-origin",
|
||||
});
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Toolbox applications" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["invalid catalog", async () => json({ schemaVersion: 2 })],
|
||||
["fetch failure", async () => json({}, 503)],
|
||||
])("survives %s in standalone mode", async (_label, fetchImplementation) => {
|
||||
const onContextError = vi.fn();
|
||||
render(
|
||||
<AppShell
|
||||
app={currentApp}
|
||||
contextOptions={{
|
||||
location:
|
||||
"https://tools.example.test/pdf/?toolbox=/toolbox.catalog.json",
|
||||
fetch: fetchImplementation,
|
||||
}}
|
||||
onContextError={onContextError}
|
||||
>
|
||||
Still usable
|
||||
</AppShell>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(onContextError).toHaveBeenCalledOnce());
|
||||
expect(screen.getByText("Still usable")).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector("[data-toolbox-context='standalone']"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user