Files
govoplan-core/webui/tests/layout-primitives.test.tsx
T
zemion 6e518fa6a2
Module Package Release / publish-packages (push) Successful in 13s
feat: compose stable product surfaces
2026-08-24 13:21:24 +02:00

275 lines
16 KiB
TypeScript

function assert(condition: unknown, message = "assertion failed"): void {
if (!condition) throw new Error(message);
}
import { renderToStaticMarkup } from "react-dom/server";
import ActionToolbar, { ToolbarGroup, ToolbarSpacer } from "../src/components/ActionToolbar";
import ContentGrid, { FormGrid, FormLayout, GridItem } from "../src/components/ContentGrid";
import ContentSection from "../src/components/ContentSection";
import Dialog from "../src/components/Dialog";
import { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
import DescriptionList, { DescriptionItem } from "../src/components/DescriptionList";
import DefinitionNodeIcon from "../src/components/DefinitionNodeIcon";
import DefinitionPalette, { DefinitionPaletteGroup, DefinitionPaletteItem } from "../src/components/DefinitionPalette";
import FilterBar from "../src/components/FilterBar";
import FloatingStatus from "../src/components/FloatingStatus";
import FormSection from "../src/components/FormSection";
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
import MetricGrid from "../src/components/MetricGrid";
import PageActionBar from "../src/components/PageActionBar";
import SelectionList, { SelectionListItem, SelectionListItemContent } from "../src/components/SelectionList";
import StatePanel from "../src/components/StatePanel";
import ProductAvailabilityState from "../src/components/ProductAvailabilityState";
import WorkspaceLayout from "../src/components/WorkspaceLayout";
import WorkspaceFrame from "../src/components/WorkspaceFrame";
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
// @ts-expect-error Refreshable pages must provide a Reload action.
const refreshableWithoutReload = <PageActionBar variant="detail" refreshable />;
// @ts-expect-error Editor pages must declare dirty state and persistence actions.
const editorWithoutPersistence = <PageActionBar variant="editor" />;
void refreshableWithoutReload;
void editorWithoutPersistence;
const toolbarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<ActionToolbar label="Object actions" density="compact" justify="between" surface="subtle" interfaceId="object.toolbar">
<ToolbarGroup grow>Filters</ToolbarGroup>
<ToolbarSpacer />
<ToolbarGroup align="end">Actions</ToolbarGroup>
</ActionToolbar>
</PlatformLanguageProvider>
);
assert(toolbarMarkup.includes("action-toolbar-wrap-responsive"), "toolbars use the responsive central wrapping policy by default");
assert(toolbarMarkup.includes('role="toolbar"'), "named toolbars expose toolbar semantics");
assert(toolbarMarkup.includes('aria-label="Object actions"'), "named toolbars expose their accessible name");
assert(toolbarMarkup.includes('data-help-scope="toolbar"'), "toolbar help scope is centralized");
assert(toolbarMarkup.includes("action-toolbar-group-grow"), "toolbar groups can own flexible space");
assert(toolbarMarkup.includes("action-toolbar-justify-between"), "toolbar distribution is centrally controlled");
assert(toolbarMarkup.includes("action-toolbar-spacer"), "the standard action spacer is available");
const editorActionBarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<PageActionBar
variant="editor"
refreshable
state="dirty"
label="Editor actions"
reloadAction={{ onReload: () => undefined, label: "Reload" }}
contextActions={<button type="button">Preview</button>}
helpAction={<button type="button">Help</button>}
destructiveActions={<button type="button">Delete</button>}
discardAction={{ label: "Discard" }}
saveAction={{ label: "Save" }}
/>
</PlatformLanguageProvider>
);
assert(editorActionBarMarkup.includes('data-page-action-archetype="editor"'), "page action bars expose their semantic archetype");
assert(editorActionBarMarkup.includes('aria-label="Editor actions"'), "page action bars are named toolbars");
assert(editorActionBarMarkup.includes('data-page-refreshable="true"'), "refreshable pages expose their refresh contract");
assert(editorActionBarMarkup.includes('data-page-dirty="true"'), "editors expose their dirty state");
assert(editorActionBarMarkup.includes('data-page-dirty-state="dirty"'), "dirty editors announce unsaved changes");
assert(editorActionBarMarkup.indexOf('data-page-action-slot="reload"') < editorActionBarMarkup.indexOf('data-page-action-slot="context"'), "reload precedes contextual actions");
assert(editorActionBarMarkup.indexOf('data-page-action-slot="help"') < editorActionBarMarkup.indexOf('data-page-action-slot="discard"'), "help precedes editor persistence actions");
assert(editorActionBarMarkup.indexOf('data-page-action-slot="destructive"') < editorActionBarMarkup.indexOf('data-page-action-slot="discard"'), "destructive editor actions are separated from persistence actions");
assert(editorActionBarMarkup.includes('data-page-action-separation="destructive"'), "destructive actions expose their visual boundary");
assert(editorActionBarMarkup.includes('role="group" aria-label="Destructive actions"'), "destructive actions expose an accessible named group");
assert(editorActionBarMarkup.indexOf('data-page-action-slot="discard"') < editorActionBarMarkup.indexOf('data-page-action-slot="save"'), "save remains the far-right editor action");
const cleanEditorActionBarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<PageActionBar
variant="editor"
state="clean"
discardAction={{ label: "Discard" }}
saveAction={{ label: "Save" }}
/>
</PlatformLanguageProvider>
);
assert(cleanEditorActionBarMarkup.includes('data-page-dirty-state="clean"'), "clean editors announce their persisted state");
assert((cleanEditorActionBarMarkup.match(/disabled=""/g) ?? []).length === 2, "clean editors keep Save and Discard visible but disabled");
assert((cleanEditorActionBarMarkup.match(/disabled-action-tooltip/g) ?? []).length === 2, "clean editor actions expose explanations for unavailable persistence");
const collectionActionBarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<PageActionBar
variant="collection"
refreshable
reloadAction={{ onReload: () => undefined, label: "Reload" }}
createAction={<button type="button">Create</button>}
/>
</PlatformLanguageProvider>
);
assert(collectionActionBarMarkup.indexOf('data-page-action-slot="reload"') < collectionActionBarMarkup.indexOf('data-page-action-slot="create"'), "collection creation remains the far-right action");
const workspaceEditorActionBarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<WorkspaceActionBar
scope="editor-pane"
variant="editor"
state="invalid"
refreshable
reloadAction={{ onReload: () => undefined, loading: true }}
destructiveActions={<button type="button">Delete</button>}
discardAction={{ label: "Discard" }}
saveAction={{ label: "Save" }}
/>
</PlatformLanguageProvider>
);
assert(workspaceEditorActionBarMarkup.includes('data-workspace-action-scope="editor-pane"'), "workspace actions expose their pane scope");
assert(workspaceEditorActionBarMarkup.includes('data-page-refresh-state="reloading"'), "reload activity is centrally projected");
assert(workspaceEditorActionBarMarkup.includes('data-page-dirty-state="invalid"'), "invalid editor state remains visible");
assert((workspaceEditorActionBarMarkup.match(/disabled=""/g) ?? []).length >= 2, "invalid editors disable Reload while active and Save while invalid");
const gridMarkup = renderToStaticMarkup(
<ContentGrid columns={3} gap="compact" collapseAt="wide">
<GridItem>A</GridItem>
<GridItem span="two">B</GridItem>
<GridItem span="full">C</GridItem>
</ContentGrid>
);
assert(gridMarkup.includes("content-grid-columns-3"), "content grids expose explicit column geometry");
assert(gridMarkup.includes("content-grid-collapse-wide"), "content grids expose explicit collapse ownership");
assert(gridMarkup.includes("content-grid-item-full"), "grid items can span the complete layout");
const sectionMarkup = renderToStaticMarkup(<ContentSection density="compact" layout="stack" surface="subtle">Section</ContentSection>);
assert(sectionMarkup.includes("content-section-spacing-block"), "content sections own standard block rhythm");
assert(sectionMarkup.includes("content-section-surface-subtle"), "content sections expose central surface variants");
assert(sectionMarkup.includes("content-section-layout-stack"), "content sections can own compact stacked evidence anatomy");
const metricMarkup = renderToStaticMarkup(
<MetricGrid columns="auto" density="compact" spacing="inset" minimum="compact" collapseAt="standard">
<div>Open</div><div>Closed</div>
</MetricGrid>
);
assert(metricMarkup.includes("metric-group-columns-auto"), "metric groups own responsive column geometry");
assert(metricMarkup.includes("metric-group-spacing-inset"), "metric groups own their surrounding rhythm");
assert(metricMarkup.includes("metric-group-collapse-standard"), "metric groups expose explicit collapse ownership");
const descriptionMarkup = renderToStaticMarkup(
<DescriptionList columns={3} density="compact" collapseAt="wide">
<DescriptionItem term="Status">Ready</DescriptionItem>
</DescriptionList>
);
assert(descriptionMarkup.startsWith('<dl class="description-list-layout'), "description lists preserve native list semantics");
assert(descriptionMarkup.includes("description-list-columns-3"), "description lists own their column geometry");
assert(descriptionMarkup.includes("<dt>Status</dt><dd>Ready</dd>"), "description items preserve term and description anatomy");
const inlineDescriptionMarkup = renderToStaticMarkup(
<DescriptionList variant="inline" termWidth="compact"><DescriptionItem term="Owner">Ada</DescriptionItem></DescriptionList>
);
assert(inlineDescriptionMarkup.includes("description-list-columns-1"), "inline description lists default to one list column");
assert(inlineDescriptionMarkup.includes("description-list-term-compact"), "inline description lists centralize term sizing");
const workspaceMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<WorkspaceLayout
variant="split"
surface="contained"
primaryLabel="Objects"
primary={<SelectionList variant="navigation" label="Objects"><SelectionListItem selected><SelectionListItemContent title="Object" description="Draft" /></SelectionListItem></SelectionList>}
>
<StatePanel size="fill" title="No object selected" actions={<button type="button">Create</button>} />
</WorkspaceLayout>
</PlatformLanguageProvider>
);
assert(workspaceMarkup.includes("workspace-layout-surface-contained"), "bounded list-detail workspaces share their surface anatomy");
assert(workspaceMarkup.includes("selection-list-navigation"), "resource navigation lists share selection geometry");
assert(workspaceMarkup.includes("selection-list-item-content"), "selection-list copy owns title and description typography");
assert(workspaceMarkup.includes("state-panel-size-fill"), "whole-surface states share sizing and action anatomy");
const availabilityMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<ProductAvailabilityState
state="degraded"
explanation={{
reason: "provider_degraded",
title: "Messages are delayed",
description: "Saved messages remain available, but new provider results may be delayed.",
resolution: "Retry later or contact the integration operator.",
responsibleRole: "Integration operator"
}}
technical={{ moduleId: "mail", providerId: "smtp-primary", correlationId: "event-1" }}
actions={<button type="button">Retry</button>}
/>
</PlatformLanguageProvider>
);
assert(availabilityMarkup.includes("product-availability-degraded"), "product availability uses one semantic state primitive");
assert(availabilityMarkup.includes("Retry later or contact the integration operator."), "availability states include an actionable recovery path");
assert(availabilityMarkup.includes("<details"), "technical provenance remains available on demand");
assert(availabilityMarkup.includes("smtp-primary"), "technical details preserve exact provider provenance");
const frameMarkup = renderToStaticMarkup(
<PlatformLanguageProvider><WorkspaceFrame as="main" height="viewport" label="Planning workspace" surface="panel"><span>Body</span></WorkspaceFrame></PlatformLanguageProvider>
);
assert(frameMarkup.includes("workspace-frame-surface-panel"), "full-height module frames share overflow and surface anatomy");
assert(frameMarkup.startsWith("<main"), "workspace frames preserve the owning page landmark");
assert(frameMarkup.includes("workspace-frame-height-viewport"), "workspace frames centralize application viewport sizing");
assert(frameMarkup.includes('aria-label="Planning workspace"'), "workspace frames expose their accessible name");
const definitionMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<DefinitionPalette label="Nodes" description="Drag onto the canvas">
<DefinitionPaletteGroup label="Sources">
<DefinitionPaletteItem icon={<DefinitionNodeIcon>DB</DefinitionNodeIcon>} label="Database" />
</DefinitionPaletteGroup>
</DefinitionPalette>
<FloatingStatus>Working...</FloatingStatus>
</PlatformLanguageProvider>
);
assert(definitionMarkup.includes("definition-palette-items"), "definition editors share bounded palette anatomy");
assert(definitionMarkup.includes("definition-node-icon"), "definition nodes share icon geometry");
assert(definitionMarkup.includes("floating-status"), "floating activity state uses the central overlay");
const filterMarkup = renderToStaticMarkup(
<FilterBar as="form" surface="panel" width="compact"><input aria-label="Search" /><button type="submit">Find</button></FilterBar>
);
assert(filterMarkup.startsWith('<form class="filter-bar'), "filter bars can preserve native form semantics");
assert(filterMarkup.includes("filter-bar-surface-panel"), "filter placement and surface are centralized");
const formMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<FormSection title="Identity" description="Stable identifying values" actions={<button type="button">Reset</button>}>
<FormGrid columns={2}><label>Name<input /></label><label>Code<input /></label></FormGrid>
</FormSection>
</PlatformLanguageProvider>
);
assert(formMarkup.includes("form-section-header"), "form sections centralize heading and action placement");
assert(formMarkup.includes("form-grid-layout"), "forms compose the central content grid");
assert(formMarkup.includes("content-grid-columns-2"), "form columns remain explicit");
const formElementMarkup = renderToStaticMarkup(<FormLayout columns={2} id="object-form"><label>Value<input /></label></FormLayout>);
assert(formElementMarkup.startsWith('<form id="object-form"'), "form layouts preserve native form semantics");
assert(formElementMarkup.includes("form-grid-layout"), "native forms share the central grid geometry");
const dialogMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<Dialog
open
title="Edit object"
description="Review the values before saving."
notices={<div role="status">Draft</div>}
size="wide"
variant="administration"
footer={<><button type="button">Cancel</button><button type="submit">Save</button></>}
>
<DialogForm><DialogSection variant="inset">Fields</DialogSection></DialogForm>
</Dialog>
</PlatformLanguageProvider>
);
assert(dialogMarkup.includes("dialog-panel-size-wide"), "dialog size is owned by the central anatomy");
assert(dialogMarkup.includes("dialog-panel-administration"), "dialog variants are explicit");
assert(dialogMarkup.includes("dialog-description"), "dialog introductions use the central region");
assert(dialogMarkup.includes("dialog-notices"), "dialog notices use the central region");
assert(dialogMarkup.includes("dialog-actions dialog-actions-end"), "every dialog footer composes the central action layout");
assert(dialogMarkup.includes("dialog-form-layout"), "dialog forms use the central vertical flow");
assert(dialogMarkup.includes("dialog-section-inset"), "dialog sections use central grouping variants");