fix(ui): unify heading help, table sizing and navigation contracts
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
Executable
+112
@@ -0,0 +1,112 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { transformSync } = require("esbuild");
|
||||
const React = require("react");
|
||||
const { renderToStaticMarkup } = require("react-dom/server");
|
||||
const { MemoryRouter } = require("react-router");
|
||||
|
||||
function loadSource(path, bindings = {}) {
|
||||
const context = vm.createContext({ module: { exports: {} }, require, ...bindings });
|
||||
context.exports = context.module.exports;
|
||||
vm.runInContext(transformSync(readFileSync(new URL(path, import.meta.url), "utf8"), {
|
||||
loader: path.endsWith(".tsx") ? "tsx" : "ts", format: "cjs", jsx: "automatic"
|
||||
}).code, context);
|
||||
return { exports: context.module.exports, context };
|
||||
}
|
||||
|
||||
const { generatedTranslations } = loadSource("../src/i18n/generatedTranslations.ts").exports;
|
||||
const launch = loadSource("../src/platform/launchContext.ts").exports;
|
||||
|
||||
function harness(locale = "en") {
|
||||
const navigation = [];
|
||||
const translateText = (value) => {
|
||||
if (typeof value === "string") return generatedTranslations[locale][value] ?? value;
|
||||
let text = generatedTranslations[locale][value.key] ?? value.key;
|
||||
for (const [key, replacement] of Object.entries(value.values)) {
|
||||
text = text.replaceAll(`{${key}}`, replacement);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
const loaded = loadSource("../src/layout/BreadcrumbBar.tsx", {
|
||||
window: { history: { state: { idx: 7 } } },
|
||||
require: (name) => {
|
||||
if (name === "../i18n/LanguageContext") return {
|
||||
usePlatformLanguage: () => ({ translateText }),
|
||||
i18nMessage: (key, values) => ({ key, values })
|
||||
};
|
||||
if (name === "../components/UnsavedChangesGuard") return {
|
||||
useGuardedNavigate: () => (...args) => navigation.push(args)
|
||||
};
|
||||
if (name === "../platform/launchContext") return launch;
|
||||
return require(name);
|
||||
}
|
||||
});
|
||||
const BreadcrumbBar = loaded.exports.default;
|
||||
return {
|
||||
...loaded, navigation, BreadcrumbBar,
|
||||
links(pathname, search = "") {
|
||||
const html = renderToStaticMarkup(React.createElement(
|
||||
MemoryRouter, { initialEntries: [`${pathname}${search}`] },
|
||||
React.createElement(BreadcrumbBar, { pathname })
|
||||
));
|
||||
return [...html.matchAll(/<a\b([^>]*)>(.*?)<\/a>/g)].map(([, attributes, label]) => ({
|
||||
href: /href="([^"]*)"/.exec(attributes)?.[1], label: label.replaceAll("&", "&")
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test("module-global reports and operator queue have their own bilingual labels and exact links", () => {
|
||||
for (const locale of ["en", "de"]) {
|
||||
const h = harness(locale);
|
||||
const root = generatedTranslations[locale]["i18n:govoplan-core.campaigns.01a23a28"];
|
||||
for (const [route, key] of [
|
||||
["reports", "i18n:govoplan-core.reports.88bc3fe3"],
|
||||
["queue", "i18n:govoplan-core.operator_queue.72492fb5"]
|
||||
]) {
|
||||
for (const trailingSlash of ["", "/"]) {
|
||||
assert.deepEqual(h.links(`/campaigns/${route}${trailingSlash}`, "?campaign=selected-campaign"), [
|
||||
{ href: "/campaigns", label: root },
|
||||
{ href: `/campaigns/${route}`, label: generatedTranslations[locale][key] }
|
||||
]);
|
||||
}
|
||||
}
|
||||
assert.equal(h.links("/operator")[0].label, generatedTranslations[locale]["i18n:govoplan-core.operator_queue.72492fb5"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("campaign editor report aliases and attachment deep links retain their singular campaign context", () => {
|
||||
const h = harness();
|
||||
const campaignId = "8ef65230-94d1-49a5-93a9-c5ef7c87ad45";
|
||||
for (const suffix of ["report", "reports", "attachments"]) {
|
||||
const links = h.links(`/campaigns/${campaignId}/${suffix}`);
|
||||
assert.deepEqual(links.map((item) => item.href), ["/campaigns", `/campaigns/${campaignId}`, `/campaigns/${campaignId}/${suffix}`]);
|
||||
assert.equal(links[1].label, generatedTranslations.en["i18n:govoplan-core.campaign.69390e16"]);
|
||||
assert.equal(links[2].label, generatedTranslations.en[suffix === "attachments"
|
||||
? "i18n:govoplan-core.attachments.6771ade6" : "i18n:govoplan-core.report.ee45c303"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("quick-access return preserves guarded history back or the exact origin deep link", () => {
|
||||
const origin = launch.createQuickAccessLaunchContext({
|
||||
pathname: "/cases/case-fixture", search: "?tab=work", hash: "#campaigns", historyIndex: 6,
|
||||
auth: { user: { account_id: "account-fixture" }, tenant: { id: "tenant-fixture" } },
|
||||
temporalContext: { validityMode: "current" }
|
||||
});
|
||||
const h = harness();
|
||||
for (const historyIndex of [7, 20]) {
|
||||
h.context.window.history.state.idx = historyIndex;
|
||||
const tree = h.BreadcrumbBar({ pathname: "/campaigns/reports", locationState: launch.quickAccessLaunchState(origin) });
|
||||
const button = React.Children.toArray(tree.props.children).find((item) => item.type === "button");
|
||||
assert.ok(button, "the launch return action remains visible");
|
||||
button.props.onClick();
|
||||
}
|
||||
assert.deepEqual(h.navigation[0], [-1]);
|
||||
assert.equal(h.navigation[1][0], "/cases/case-fixture?tab=work#campaigns");
|
||||
assert.equal(h.navigation[1][1].replace, true);
|
||||
});
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { componentSuites, runComponentTests, selectSuites } from "../scripts/run-component-tests.mjs";
|
||||
|
||||
test("component aliases select the existing contract and reject unknown input", () => {
|
||||
assert.deepEqual(selectSuites([]), Object.keys(componentSuites));
|
||||
assert.deepEqual(selectSuites(["page-layout", "page-layout"]), ["page-layout"]);
|
||||
assert.deepEqual(componentSuites["data-grid-actions"], ["data-grid-actions", "data-grid-sizing"]);
|
||||
assert.throws(() => selectSuites(["../../unowned"]), /Unknown component suite/);
|
||||
});
|
||||
|
||||
test("every configured component test remains covered by the batch and standalone aliases", () => {
|
||||
const config = JSON.parse(readFileSync(new URL("../tsconfig.component-tests.json", import.meta.url), "utf8"));
|
||||
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
||||
const configured = config.include.filter((file) => file.startsWith("tests/")).map((file) => file.replace(/^tests\//, "").replace(/\.test\.tsx?$/, "")).sort();
|
||||
assert.deepEqual(Object.values(componentSuites).flat().sort(), configured);
|
||||
for (const name of Object.keys(componentSuites)) {
|
||||
assert.equal(packageJson.scripts[`test:${name}`], `node scripts/run-component-tests.mjs ${name}`);
|
||||
}
|
||||
assert.equal(packageJson.scripts["test:components"], "node scripts/run-component-tests.mjs");
|
||||
});
|
||||
|
||||
test("one compile serves a batch and preserves structural follow-ups", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "govoplan-component-runner-"));
|
||||
const commands = [];
|
||||
try {
|
||||
const result = await runComponentTests({ webuiRoot: root, compiler: "fixture-tsc", names: ["data-grid-actions", "dialog-focus"], run: async (argv) => commands.push(argv) });
|
||||
assert.equal(result.compiled, 1);
|
||||
assert.equal(commands.filter((argv) => argv[1] === "fixture-tsc").length, 1);
|
||||
assert.equal(commands.length, 5);
|
||||
assert(commands.at(-1)[1].endsWith("test-dialog-focus-structure.mjs"));
|
||||
assert.deepEqual(readdirSync(root), []);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("overlapping runs have separate outputs and failures clean only owned output", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "govoplan-component-runner-"));
|
||||
const outputs = [];
|
||||
let unblock;
|
||||
const bothStarted = new Promise((resolve) => { unblock = resolve; });
|
||||
const run = async (argv) => {
|
||||
if (argv[1] !== "fixture-tsc") return;
|
||||
outputs.push(argv.at(-1));
|
||||
if (outputs.length === 2) unblock();
|
||||
await bothStarted;
|
||||
throw new Error("fixture compilation failed");
|
||||
};
|
||||
try {
|
||||
const results = await Promise.allSettled([1, 2].map(() => runComponentTests({ webuiRoot: root, compiler: "fixture-tsc", names: ["page-layout"], run })));
|
||||
assert(results.every((result) => result.status === "rejected"));
|
||||
assert.equal(new Set(outputs).size, 2);
|
||||
assert.deepEqual(readdirSync(root), []);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -342,6 +342,77 @@ assertWidths(
|
||||
"growth does not resize a fixed peer merely to avoid overflow"
|
||||
);
|
||||
|
||||
const mixedPeerColumns: DataGridSizingColumn[] = [
|
||||
{ id: "first", width: 200, minWidth: 100, maxWidth: 500, resizable: true },
|
||||
{ id: "fixed", width: 120, minWidth: 120, maxWidth: 120 },
|
||||
{ id: "last", width: 300, minWidth: 100, maxWidth: 500, resizable: true },
|
||||
{ id: "actions", width: 100, sticky: "end" }
|
||||
];
|
||||
const mixedPeerBase = { first: 200, fixed: 120, last: 300, actions: 100 };
|
||||
for (const mode of ["cover", "free", "constrained"] as const) {
|
||||
const grown = resizeDataGridColumn(mixedPeerColumns, mixedPeerBase, "first", 80, mode);
|
||||
assertWidths(grown.widths,
|
||||
{ first: 280, fixed: 120, last: mode === "constrained" ? 220 : 300, actions: 100 },
|
||||
`${mode} growth crosses a fixed intermediate column without changing it`);
|
||||
const shrunk = resizeDataGridColumn(mixedPeerColumns, mixedPeerBase, "first", -80, mode);
|
||||
assertWidths(shrunk.widths,
|
||||
{ first: 120, fixed: 120, last: mode === "free" ? 300 : 380, actions: 100 },
|
||||
`${mode} shrink uses only the appropriate resizable compensation peers`);
|
||||
const limited = resizeDataGridColumn(mixedPeerColumns, mixedPeerBase, "first", 800, mode);
|
||||
assertEqual(limited.widths.fixed, 120, `${mode} exhaustion never changes the fixed intermediate track`);
|
||||
assertEqual(limited.widths.first, mode === "constrained" ? 400 : 500, `${mode} retains declared growth limits`);
|
||||
}
|
||||
|
||||
const preferredLimitColumns: DataGridSizingColumn[] = [
|
||||
{ id: "recipients", width: "minmax(320px, 1.4fr)", preferredMaxWidth: 640, resizable: true },
|
||||
{ id: "active", width: 130 },
|
||||
{ id: "delivery", width: "minmax(260px, 0.9fr)", preferredMaxWidth: 480, resizable: true },
|
||||
{ id: "attachments", width: 180 },
|
||||
...["first", "second", "third"].map((id) => ({ id, width: 190, minWidth: 160, preferredMaxWidth: 360, resizable: true })),
|
||||
{ id: "actions", width: 180, sticky: "end" }
|
||||
];
|
||||
const hardLimitColumns = preferredLimitColumns.map(({ preferredMaxWidth, ...column }) => ({ ...column, maxWidth: preferredMaxWidth }));
|
||||
for (const container of [900, 1600, 2400, 3085]) {
|
||||
assertWidths(fitDataGridColumns(preferredLimitColumns, container).widths,
|
||||
fitDataGridColumns(hardLimitColumns, container).widths,
|
||||
`preferred caps preserve the previous automatic fit at ${container}px without imposing manual caps`);
|
||||
}
|
||||
const rightPeersAtOldCaps: DataGridSizingColumn[] = [
|
||||
{ id: "recipients", width: "minmax(320px, 1.4fr)", preferredMaxWidth: 640, resizable: true },
|
||||
{ id: "fixed", width: 130, minWidth: 130, maxWidth: 130 },
|
||||
{ id: "first", width: 190, preferredMaxWidth: 360, resizable: true },
|
||||
{ id: "second", width: 190, preferredMaxWidth: 360, resizable: true },
|
||||
{ id: "actions", width: 180, sticky: "end" }
|
||||
];
|
||||
const oversizedRecipient = { recipients: 1000, fixed: 130, first: 360, second: 360, actions: 180 };
|
||||
const oldManualCaps = rightPeersAtOldCaps.map(({ preferredMaxWidth, ...column }) => ({
|
||||
...column, maxWidth: preferredMaxWidth ?? column.maxWidth
|
||||
}));
|
||||
assertEqual(resizeDataGridColumn(oldManualCaps, oversizedRecipient, "recipients", -120, "cover", 0).appliedDelta, 0,
|
||||
"regression: at the scrolled-right boundary, old field caps block shrinking an oversized recipient column");
|
||||
const shrunkAcrossPreferredCaps = resizeDataGridColumn(rightPeersAtOldCaps, oversizedRecipient, "recipients", -120, "cover", 0);
|
||||
assertWidths(shrunkAcrossPreferredCaps.widths,
|
||||
{ recipients: 880, fixed: 130, first: 420, second: 420, actions: 180 },
|
||||
"preferred field caps allow cover compensation at the right scroll boundary without moving fixed neighbors");
|
||||
assertWidths(fitDataGridColumns(rightPeersAtOldCaps, 2030, {}, shrunkAcrossPreferredCaps.widths, "cover", 2030).widths,
|
||||
shrunkAcrossPreferredCaps.widths, "committing a compensated shrink never refills the wide recipient column");
|
||||
for (const mode of ["cover", "free", "constrained"] as const) {
|
||||
const expandedField = resizeDataGridColumn(rightPeersAtOldCaps, oversizedRecipient, "first", 160, mode);
|
||||
assertEqual(expandedField.widths.first, 520, `${mode} direct resizing can exceed a presentation-only field cap`);
|
||||
assertEqual(expandedField.widths.fixed, 130, `${mode} preferred caps do not change a fixed neighbor`);
|
||||
}
|
||||
const preferredWithHardLimit: DataGridSizingColumn[] = [{ id: "text", width: 300, preferredMaxWidth: 360, maxWidth: 500, resizable: true }];
|
||||
assertEqual(resizeDataGridColumn(preferredWithHardLimit, { text: 360 }, "text", 200, "cover").widths.text, 500,
|
||||
"an explicitly declared hard maximum still bounds direct resizing beyond a preferred cap");
|
||||
assertEqual(fitDataGridColumns([{ id: "text", width: 500, preferredMaxWidth: 400, maxWidth: 300 }], 300, {}, {}, "free").widths.text, 300,
|
||||
"a preferred cap never overrides a smaller hard maximum");
|
||||
assertEqual(fitDataGridColumns([{ id: "text", width: 300, minWidth: 160, preferredMaxWidth: 100 }], 160, {}, {}, "free").widths.text, 160,
|
||||
"preferred caps never reduce a column below its hard minimum");
|
||||
assertEqual(dataGridLayoutSignature([{ id: "text", width: 190, resizable: true }], "container", "cover"),
|
||||
"v3::text:190:::r::::default:::container::cover", "omitting preferred caps preserves the existing persisted signature format");
|
||||
assertEqual(dataGridLayoutSignature(preferredLimitColumns, "container", "cover") === dataGridLayoutSignature(hardLimitColumns, "container", "cover"), false,
|
||||
"new preferred sizing declarations invalidate only their own obsolete manual-width contract");
|
||||
|
||||
const coverBeyondPassiveMax = resizeDataGridColumn([
|
||||
{ id: "first", width: 200, minWidth: 100, maxWidth: 200, resizable: true },
|
||||
{ id: "second", width: 300, minWidth: 100, maxWidth: 300, resizable: true },
|
||||
|
||||
@@ -6,6 +6,14 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import DocumentationHelpLink, { DocumentationHelpProvider } from "../src/components/help/DocumentationHelpLink";
|
||||
import FieldLabel from "../src/components/help/FieldLabel";
|
||||
import { documentationHelpHref } from "../src/components/help/documentationHelp";
|
||||
import PageLayout from "../src/components/PageLayout";
|
||||
import PageTitle from "../src/components/PageTitle";
|
||||
import Card from "../src/components/Card";
|
||||
import Dialog from "../src/components/Dialog";
|
||||
import AdminPageLayout from "../src/components/admin/AdminPageLayout";
|
||||
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
|
||||
import TextWithHelp from "../src/components/help/TextWithHelp";
|
||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||
|
||||
assert(
|
||||
documentationHelpHref({ topicId: "campaigns.workflow.complete-review" }) ===
|
||||
@@ -49,3 +57,30 @@ const fieldMarkup = renderToStaticMarkup(
|
||||
</DocumentationHelpProvider>
|
||||
);
|
||||
assert(fieldMarkup.includes("topic=access.reference.admin-access-fields"), "field labels can link to stable reference topics");
|
||||
|
||||
for (const language of ["en", "de"]) {
|
||||
const helpLabel = language === "de" ? "Benutzerdokumentation öffnen" : "Open user documentation";
|
||||
const help = <DocumentationHelpLink reference={{ contextId: "dashboard" }} />;
|
||||
for (const [name, element] of [
|
||||
["page", <PageLayout archetype="overview" title="Dashboard" titleHelp={help} headerLoading actions={<button>Configure</button>}>Data</PageLayout>],
|
||||
["title", <PageTitle loading titleHelp={help}>Dashboard</PageTitle>],
|
||||
["administration", <AdminPageLayout title="Dashboard" description="Summary" titleHelp={help}>Data</AdminPageLayout>],
|
||||
["card", <Card title="Dashboard" titleHelp={help} collapsible>Data</Card>],
|
||||
["dialog", <Dialog open title="Dashboard" titleHelp={help} onClose={() => undefined}>Data</Dialog>],
|
||||
["workspace", <WorkspaceActionBar variant="workspace" title="Dashboard" titleHelp={help} titleLevel={1} primaryActions={<button>Edit</button>} />],
|
||||
["section", <TextWithHelp as="div" help={help}><h3>Dashboard</h3></TextWithHelp>]
|
||||
] as const) {
|
||||
const markup = renderToStaticMarkup(<PlatformLanguageProvider preferredLanguageCode={language}>{element}</PlatformLanguageProvider>);
|
||||
const heading = markup.match(/<h[123]\b[^>]*>[\s\S]*?<\/h[123]>/)?.[0];
|
||||
assert(heading?.includes("Dashboard"), `${name} retains a visible semantic heading`);
|
||||
assert(!heading?.includes("documentation-help-link") && !heading?.includes("loading-indicator"), `${name} heading name excludes help and progress controls`);
|
||||
assert(markup.includes(`aria-label="${helpLabel}"`), `${name} keeps translated documentation access in ${language}`);
|
||||
assert(markup.indexOf('class="documentation-help-link"') > markup.indexOf(heading!), `${name} help follows its visible heading`);
|
||||
assert(!/<button\b[^>]*>[\s\S]*?documentation-help-link[\s\S]*?<\/button>/.test(markup), `${name} never nests documentation inside an action`);
|
||||
}
|
||||
}
|
||||
|
||||
const titleWithoutText = renderToStaticMarkup(<WorkspaceActionBar variant="workspace" titleHelp={<DocumentationHelpLink reference={{ contextId: "dashboard" }} />} />);
|
||||
assert(!titleWithoutText.includes("documentation-help-link"), "workspace help cannot appear without a visible context heading");
|
||||
const unheadedCard = renderToStaticMarkup(<Card titleHelp={<DocumentationHelpLink reference={{ contextId: "dashboard" }} />}>Data</Card>);
|
||||
assert(!unheadedCard.includes("documentation-help-link"), "card help cannot appear without a visible title");
|
||||
|
||||
@@ -346,6 +346,28 @@ const aliasOrdered = groupNavigationItems(reorderedProductNavigation.primaryItem
|
||||
assert(aliasOrdered[0]?.items[0]?.to === "/files" && aliasOrdered[1]?.items[0]?.to === "/messages" && aliasOrdered[1]?.label === "View group", "View ordering recognizes non-placement owner aliases and assigns the requested separator exactly once");
|
||||
const unauthorizedLockedOwner = projectProductNavigation(reorderedOwners, messageSurfaceModules, { scopes: ["mail:mailbox:read"] } as AuthInfo).primaryItems.find((item) => item.to === "/messages");
|
||||
assert(unauthorizedLockedOwner?.navigationLocked === false && !unauthorizedLockedOwner.navigationAliases?.includes("postbox.navigation.postbox"), "unavailable optional owners contribute neither aliases nor lock metadata");
|
||||
const composedCommunicationArea = [{
|
||||
id: "communication", moduleId: "mail", label: "Communication", iconName: "mail" as const,
|
||||
surfaceIds: ["mail.navigation.mail"], order: 40
|
||||
}];
|
||||
const defaultComposedNavigation = reorderedProductNavigation.primaryItems.map((item) => ({
|
||||
...item, navigationCustomLayout: false, navigationSection: null
|
||||
}));
|
||||
assert(groupNavigationItems(defaultComposedNavigation, composedCommunicationArea)[0]?.items[0]?.to === "/messages", "standard product-area grouping recognizes authorized composed-owner aliases even when another owner supplies the rail placement");
|
||||
assert(groupNavigationItems(defaultComposedNavigation, composedCommunicationArea)[0]?.label === "Communication", "composed entries retain their standard section heading without a saved layout");
|
||||
const competingOwnerAreas = [
|
||||
{ ...composedCommunicationArea[0], order: 10 },
|
||||
{ ...composedCommunicationArea[0], id: "direct-owner", label: "Direct owner area", surfaceIds: ["postbox.navigation.postbox"], order: 70 }
|
||||
];
|
||||
const directOwnerGroups = groupNavigationItems(defaultComposedNavigation, competingOwnerAreas);
|
||||
assert(directOwnerGroups[0]?.id === "product-area:direct-owner" && directOwnerGroups[0]?.items[0]?.to === "/messages", "an authorized composed entry keeps its directly declared area even when another owner's alias belongs to an earlier area");
|
||||
assert(directOwnerGroups.flatMap((group) => group.items).filter((item) => item.to === "/messages").length === 1, "competing owner areas never duplicate the composed destination");
|
||||
const personalFlatNavigation = defaultComposedNavigation.map((item) => ({
|
||||
...item, navigationCustomLayout: true, navigationLayoutSource: "user"
|
||||
}));
|
||||
assert(groupNavigationItems(personalFlatNavigation, composedCommunicationArea).every((group) => group.label === undefined), "standard sections never replace an explicitly flat personal layout");
|
||||
const mailOnlyDefaultNavigation = projectProductNavigation(reorderedOwners, messageSurfaceModules, { scopes: ["mail:mailbox:read"] } as AuthInfo).primaryItems;
|
||||
assert(groupNavigationItems(mailOnlyDefaultNavigation, [{ ...composedCommunicationArea[0], surfaceIds: ["postbox.navigation.postbox"] }])[0]?.id === "more-tools", "an unauthorized optional owner's area alias cannot affect default navigation grouping");
|
||||
assert(
|
||||
mailOnlyNavigation.primaryItems.map((item) => item.to).join(",") === "/messages",
|
||||
"composition should remain stable when only one optional contributor is authorized"
|
||||
|
||||
@@ -91,3 +91,34 @@ const delegatedHeaderMarkup = renderToStaticMarkup(
|
||||
|
||||
assert(!delegatedHeaderMarkup.includes("page-layout-header"), "composite workspaces can delegate their visible heading to contributed content");
|
||||
assert(delegatedHeaderMarkup.includes("page-layout-workspace content-pad workspace-data-page"), "headerless composite pages retain the central content frame");
|
||||
|
||||
for (const state of ["clean", "dirty", "invalid", "save-failed", "conflict", "saving"] as const) {
|
||||
for (const behavior of ["reset", "exit"] as const) {
|
||||
const markup = renderToStaticMarkup(
|
||||
<PlatformLanguageProvider>
|
||||
<PageActionBar variant="editor" state={state}
|
||||
discardAction={{ label: "Cancel", behavior, "aria-label": "test-cancel" }}
|
||||
saveAction={{ label: "Save", "aria-label": "test-save" }} />
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
const cancelButton = markup.match(/<button\b[^>]*aria-label="test-cancel"[^>]*>/)?.[0];
|
||||
const saveButton = markup.match(/<button\b[^>]*aria-label="test-save"[^>]*>/)?.[0];
|
||||
assert(cancelButton && saveButton, "editor actions remain visible in every draft state");
|
||||
const disabled = (button: string | undefined) => /\bdisabled=""/.test(button ?? "");
|
||||
assert(disabled(cancelButton) === (state === "saving" || (state === "clean" && behavior === "reset")),
|
||||
`${behavior} action has the correct disabled state when ${state}`);
|
||||
assert(disabled(saveButton) === ["clean", "invalid", "saving"].includes(state),
|
||||
`cancel availability must not enable invalid/no-op/concurrent saving when ${state}`);
|
||||
assert(!markup.includes('behavior="'), "the action contract must not leak non-HTML attributes");
|
||||
}
|
||||
}
|
||||
|
||||
const permissionBlockedExit = renderToStaticMarkup(
|
||||
<PlatformLanguageProvider>
|
||||
<PageActionBar variant="editor" state="clean"
|
||||
discardAction={{ label: "Cancel", behavior: "exit", disabled: true, disabledReason: "Explicitly blocked", "aria-label": "test-blocked-cancel" }}
|
||||
saveAction={{ label: "Save" }} />
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
assert(/\bdisabled=""/.test(permissionBlockedExit.match(/<button\b[^>]*aria-label="test-blocked-cancel"[^>]*>/)?.[0] ?? ""),
|
||||
"exit semantics preserve an explicit owning-page blocker");
|
||||
|
||||
@@ -38,7 +38,10 @@ const markup = renderToStaticMarkup(
|
||||
);
|
||||
|
||||
assert(markup.includes('role="listbox"'), "the root exposes listbox semantics");
|
||||
assert(markup.includes('class="selection-list domain-list"'), "root caller classes are preserved");
|
||||
const rootClasses = markup.match(/\bclass="([^"]+)"/)?.[1].split(/\s+/) ?? [];
|
||||
assert(rootClasses.includes("selection-list"), "the root retains its shared component class");
|
||||
assert(rootClasses.includes("selection-list-plain"), "the root defaults to the plain variant");
|
||||
assert(rootClasses.includes("domain-list"), "root caller classes are preserved");
|
||||
assert(markup.includes('data-source="inbox"'), "root native properties pass through");
|
||||
assert(markup.includes('aria-label="Notifications"'), "the accessible label is translated");
|
||||
assert(markup.includes('role="option"'), "items expose option semantics");
|
||||
@@ -59,6 +62,19 @@ const nativeLabelMarkup = renderToStaticMarkup(
|
||||
);
|
||||
assert(nativeLabelMarkup.includes('aria-label="Already translated"'), "native accessible labels pass through unchanged");
|
||||
|
||||
const navigationMarkup = renderToStaticMarkup(
|
||||
<SelectionList variant="navigation" className="domain-navigation" aria-label="Navigation">
|
||||
<SelectionListItem selected>Current section</SelectionListItem>
|
||||
</SelectionList>
|
||||
);
|
||||
const navigationClasses = navigationMarkup.match(/\bclass="([^"]+)"/)?.[1].split(/\s+/) ?? [];
|
||||
assert(navigationClasses.includes("selection-list"), "navigation retains the shared root class");
|
||||
assert(navigationClasses.includes("selection-list-navigation"), "the navigation variant is explicit");
|
||||
assert(!navigationClasses.includes("selection-list-plain"), "navigation does not inherit the plain variant class");
|
||||
assert(navigationClasses.includes("domain-navigation"), "navigation preserves caller classes");
|
||||
assert(navigationMarkup.includes('role="listbox"'), "navigation retains listbox semantics");
|
||||
assert(navigationMarkup.includes('aria-label="Navigation"'), "navigation retains its accessible label");
|
||||
|
||||
const directoryOptions = [
|
||||
{
|
||||
value: "account-1",
|
||||
|
||||
Reference in New Issue
Block a user