159 lines
15 KiB
JavaScript
159 lines
15 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const webuiRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
const read = (path) => readFileSync(resolve(webuiRoot, path), "utf8");
|
|
const require = createRequire(import.meta.url);
|
|
|
|
const settings = read("src/features/settings/SettingsPage.tsx");
|
|
const retention = read("src/features/privacy/RetentionPolicyManagement.tsx");
|
|
const confirmDialog = read("src/components/ConfirmDialog.tsx");
|
|
const credentials = read("src/components/CredentialEnvelopeManager.tsx");
|
|
const iconRail = read("src/layout/IconRail.tsx");
|
|
const moduleLoadBoundary = read("src/components/ModuleLoadBoundary.tsx");
|
|
const moduleLoading = read("src/platform/modules.ts");
|
|
const application = read("src/App.tsx");
|
|
assert.match(moduleLoading, /importModuleWithRetry\(loader\.load\)/, "local descriptor imports use one bounded retry");
|
|
assert.match(moduleLoading, /failedLocalModules\.add\(loader\.packageName\)/, "final local import failures retain an explicit unavailable state");
|
|
assert.match(moduleLoading, /onLoadFailure\?\.\(info\.id\)/, "enabled module import failures are reported to the shell");
|
|
assert.match(application, /webModuleLoadFailures\.length > 0[\s\S]*optional_module_load_failed/, "an import failure is not silently presented as an uninstalled optional module");
|
|
assert.match(application, /function WebModuleLoadFailureNotice[\s\S]*useUnsavedChanges\(\)[\s\S]*requestNavigation\(\(\) => window\.location\.reload\(\)\)/, "module recovery reload uses the shared unsaved-draft navigation guard");
|
|
const wysiwygEditor = read("src/components/WysiwygEditor.tsx");
|
|
const titlebar = read("src/layout/Titlebar.tsx");
|
|
const temporalDataMenu = read("src/layout/TemporalDataMenu.tsx");
|
|
const helpMenu = read("src/layout/HelpMenu.tsx");
|
|
const helpContext = read("src/utils/helpContext.ts");
|
|
const pageLayout = read("src/components/PageLayout.tsx");
|
|
const workspaceLayout = read("src/components/WorkspaceLayout.tsx");
|
|
const actionToolbar = read("src/components/ActionToolbar.tsx");
|
|
const pageActionBar = read("src/components/PageActionBar.tsx");
|
|
const workspaceActionBar = read("src/components/WorkspaceActionBar.tsx");
|
|
const contentGrid = read("src/components/ContentGrid.tsx");
|
|
const formSection = read("src/components/FormSection.tsx");
|
|
const dialogAnatomy = read("src/components/DialogAnatomy.tsx");
|
|
const adminPageLayout = read("src/components/admin/AdminPageLayout.tsx");
|
|
const layoutStyles = read("src/styles/layout.css");
|
|
const authGateStyles = read("src/styles/auth-gate.css");
|
|
const viteConfig = read("vite.config.ts");
|
|
const packageManifest = JSON.parse(read("package.json"));
|
|
|
|
// Component presence alone cannot detect a collection toolbar in a left pane.
|
|
await import("./test-workspace-collection-actions.mjs");
|
|
|
|
const listSelectionFilter = read("src/components/ListSelectionFilter.tsx");
|
|
const dataGrid = read("src/components/table/DataGrid.tsx");
|
|
const multiSelectFilter = read("src/components/MultiSelectFilter.tsx");
|
|
assert.match(dataGrid, /<ListSelectionFilter\b/, "DataGrid consumes the shared checkbox filter body");
|
|
assert.match(multiSelectFilter, /<ListSelectionFilter\b/, "standalone list filters consume the same checkbox body");
|
|
assert.match(listSelectionFilter, /onChange\(null\)/, "Select all explicitly removes the restriction");
|
|
assert.match(listSelectionFilter, /onChange\(\[\]\)/, "Deselect all explicitly matches nothing");
|
|
assert.match(read("src/components/Card.tsx"), /bodyLayout\?: "content" \| "table"/, "table cards explicitly own their inset contract");
|
|
|
|
// A package in resolve.dedupe is not necessarily prebundled. Check include
|
|
// specifically: lazy Campaign imports previously triggered dependency churn
|
|
// after the UI was already open, leaving module-load errors on first visits.
|
|
const optimization = viteConfig.match(/optimizeDeps:\s*\{([\s\S]*?)\n\s*\},/)?.[1];
|
|
assert.ok(optimization, "the development dependency optimizer is configured");
|
|
const optimizedVendors = [...(optimization.match(/include:\s*\[([\s\S]*?)\]/)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
for (const specifier of [
|
|
"@xyflow/react",
|
|
"read-excel-file/browser",
|
|
"read-excel-file/universal",
|
|
"@tiptap/core",
|
|
"@tiptap/extension-image",
|
|
"@tiptap/react",
|
|
"@tiptap/starter-kit"
|
|
]) {
|
|
assert.ok(optimizedVendors.includes(specifier), `${specifier} is prebundled before lazy routes open`);
|
|
const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
assert.ok(packageManifest.dependencies[packageName] ?? packageManifest.devDependencies[packageName], `${specifier} is a direct dependency of the development shell`);
|
|
assert.doesNotThrow(() => require.resolve(specifier), `${specifier} has an installed resolvable entry point`);
|
|
}
|
|
assert.match(optimization, /exclude:\s*availableWebModuleSpecifiers\(\)/, "optional module packages stay outside dependency prebundling");
|
|
assert.ok(!optimizedVendors.some((specifier) => specifier.startsWith("@govoplan/")), "vendor prebundling must not eagerly import optional module packages");
|
|
|
|
assert.match(settings, /contextId: "core\.settings"/, "settings expose stable contextual documentation");
|
|
assert.match(settings, /archetype=\{editorSection \? "editor" : "workspace"\}/, "settings declare editor intent only for draft-owning sections");
|
|
assert.match(settings, /<PageActionBar[\s\S]*variant="editor"[\s\S]*state=\{editorSaving \? "saving" : editorDirty \? "dirty" : "clean"\}[\s\S]*discardAction=[\s\S]*saveAction=/, "settings use central lifecycle-aware persistence actions");
|
|
|
|
assert.match(retention, /<ActionBlockerHint/, "retention renders the shared actionable blocker");
|
|
assert.match(retention, /contextId: "privacy\.retention"/, "retention exposes stable admin documentation");
|
|
assert.match(retention, /policy\.retention\.field\.store-raw-campaign-json/, "retention storage controls expose exact help contexts");
|
|
assert.match(retention, /policy\.retention\.field\.audit-detail-level/, "retention audit controls expose exact help contexts");
|
|
assert.match(retention, /policy\.retention\.action\.save/, "retention persistence exposes exact help contexts");
|
|
assert.match(retention, /helpModuleId="policy"/, "embedded retention controls retain Policy as their documentation owner");
|
|
assert.match(retention, /locked by platform configuration/, "retention explains platform locks");
|
|
assert.doesNotMatch(retention, /<textarea/, "retention does not use raw text or JSON as its primary editor");
|
|
assert.match(confirmDialog, /PlatformInterfaceIdentityProps/, "confirm dialogs accept stable interface help identities");
|
|
assert.match(confirmDialog, /helpContextId=\{helpContextId\}/, "confirm dialogs propagate their explicit help context");
|
|
|
|
assert.match(credentials, /<ActionBlockerHint/, "credentials render the shared actionable blocker");
|
|
assert.match(credentials, /contextId: "access\.credentials"/, "credentials expose stable admin documentation");
|
|
assert.match(credentials, /access\.credentials\.field\.secret/, "credential secrets expose exact handling guidance");
|
|
assert.match(credentials, /access\.credentials\.field\.inherit-to-lower-scopes/, "credential inheritance exposes exact scope guidance");
|
|
assert.match(credentials, /access\.credentials\.confirm-delete/, "credential deletion exposes exact consequence guidance");
|
|
assert.match(credentials, /helpModuleId="access"/, "embedded credential controls retain Access as their documentation owner");
|
|
assert.match(credentials, /disabledReason: writeDisabledReason/, "credential row actions retain actionable disabled reasons");
|
|
assert.doesNotMatch(credentials, /<textarea/, "credentials use typed controls rather than a primary JSON editor");
|
|
|
|
assert.match(pageLayout, /`page-layout-\$\{mode\}`/, "shared standalone and embedded pages use one central frame");
|
|
assert.match(pageLayout, /export type PageArchetype = "overview" \| "collection" \| "detail" \| "editor" \| "workspace"/, "page intent uses the five central archetypes");
|
|
assert.match(pageLayout, /data-page-archetype=\{archetype\}/, "shared pages expose their semantic archetype");
|
|
assert.match(pageLayout, /<PageHeader/, "shared pages use one central responsive heading");
|
|
assert.match(pageLayout, /data-help-scope="page"/, "shared pages preserve contextual-help identity");
|
|
assert.match(adminPageLayout, /<PageLayout/, "administration composes the central page layout instead of redefining it");
|
|
assert.match(workspaceLayout, /workspace-layout-\$\{variant\}/, "shared workspaces expose central navigation and split-pane variants");
|
|
assert.match(workspaceLayout, /data-help-scope="workspace"/, "shared workspaces preserve contextual-help identity");
|
|
assert.match(actionToolbar, /action-toolbar-wrap-\$\{wrap\}/, "shared toolbars own responsive wrapping");
|
|
assert.match(actionToolbar, /data-help-scope="toolbar"/, "shared toolbars preserve contextual-help identity");
|
|
assert.match(pageActionBar, /variant: "collection"/, "collection pages have a semantic action-bar contract");
|
|
assert.match(pageActionBar, /variant: "detail"/, "detail pages have a semantic action-bar contract");
|
|
assert.match(pageActionBar, /variant: "editor"/, "editors have a semantic action-bar contract");
|
|
assert.match(pageActionBar, /variant: "overview"/, "overview pages have a semantic action-bar contract");
|
|
assert.match(pageActionBar, /variant: "workspace"/, "task workspaces have a semantic action-bar contract");
|
|
assert.match(pageActionBar, /refreshable: true;\s*reloadAction: PageReloadAction;/, "refreshable pages require a typed Reload action");
|
|
assert.match(pageActionBar, /state: PageEditorState;/, "editors require an explicit persistence lifecycle state");
|
|
assert.match(pageActionBar, /data-page-dirty-state=/, "editors announce clean, dirty, and saving states");
|
|
assert.match(pageActionBar, /<ActionSlot name="reload">/, "page action bars keep reload in a named stable slot");
|
|
assert.match(pageActionBar, /data-page-action-group="trailing"[\s\S]*<ActionSlot name="reload">/, "Reload belongs to the trailing group beside primary actions");
|
|
assert.doesNotMatch(pageActionBar, /data-page-action-group="leading"(?:(?!<\/ToolbarGroup>)[\s\S])*name="reload"/, "module-independent reload placement must not drift back to the leading group");
|
|
assert.match(pageActionBar, /data-page-action-separation="destructive"/, "destructive page actions expose a separate semantic group");
|
|
assert.match(pageActionBar, /<ActionSlot name="discard">[\s\S]*<ActionSlot name="save">/, "editor save follows discard in the trailing group");
|
|
assert.match(workspaceActionBar, /actionScope=\{scope\}/, "workspace and pane action bars project their semantic scope centrally");
|
|
assert.match(workspaceActionBar, /WorkspaceActionBarProps = PageActionBarProps/, "workspace editor panes reuse the page persistence contract");
|
|
assert.match(contentGrid, /content-grid-collapse-\$\{collapseAt\}/, "shared grids make their collapse point explicit");
|
|
assert.match(formSection, /form-section-header/, "shared form sections own heading and action placement");
|
|
assert.match(dialogAnatomy, /dialog-actions-\$\{align\}/, "shared dialog anatomy owns footer action placement");
|
|
|
|
assert.match(iconRail, /<div className="icon-rail-scroll">\s*<nav className="icon-nav">/, "the module navigation has a dedicated scroll viewport");
|
|
assert.match(layoutStyles, /\.icon-rail-scroll \{[^}]*min-height: 0;[^}]*flex: 1 1 auto;[^}]*overflow-y: auto;/, "only the middle rail region scrolls");
|
|
assert.match(layoutStyles, /\.icon-rail-header \{[^}]*flex: 0 0 auto;/, "the rail logo remains fixed");
|
|
assert.match(layoutStyles, /\.icon-rail-bottom \{[^}]*flex: 0 0 auto;/, "the rail utility controls remain fixed");
|
|
|
|
assert.doesNotMatch(titlebar, /titlebar-status-pattern|TriangleAlert|WifiOff/, "shell state uses the readable text warnings rather than icon or background decoration");
|
|
assert.doesNotMatch(titlebar, /titlebar-global-search|has-global-search/, "global search no longer reserves a centered titlebar grid cell");
|
|
assert.match(titlebar, /<GlobalSearch[\s\S]*<LanguageMenu \/>[\s\S]*<ViewSelector[\s\S]*<TemporalDataMenu \/>/, "search is the first command before language, View, and temporal selectors");
|
|
assert.match(titlebar, /className="account-pill"[\s\S]*aria-label=\{displayUserName\}[\s\S]*aria-haspopup="menu"/, "the compact account menu retains an accessible name and menu state");
|
|
assert.match(temporalDataMenu, /Calendar, CalendarOff/, "the temporal selector distinguishes bounded and all-validity modes");
|
|
assert.match(temporalDataMenu, /useUnsavedChanges[\s\S]*requestNavigation/, "changing temporal context cannot silently discard a dirty page");
|
|
assert.match(temporalDataMenu, /recordedAt/, "valid time and recorded time remain independently selectable");
|
|
assert.match(layoutStyles, /\.maintenance-topbar-link,[\s\S]*\.backend-offline-topbar-alert \{[^}]*top: 50%;[^}]*transform: translate\(-50%, -50%\);/, "maintenance and offline warnings use their established centered titlebar placement");
|
|
assert.match(authGateStyles, /\.titlebar-link,[\s\S]*\.account-pill \{[^}]*margin: 0;/, "text and account titlebar controls use the same action spacing as icon controls");
|
|
assert.match(helpMenu, /helpContextForTarget\(routeHelpContext, event\.target, modules\)/, "F1 resolves the currently focused interface item");
|
|
assert.match(helpContext, /function moduleRouteContext/, "context help covers contributed module routes");
|
|
assert.match(helpContext, /function sectionContext/, "context help covers contributed administration and settings sections");
|
|
assert.match(helpContext, /data-help-context-id/, "context help honors explicit control metadata");
|
|
assert.match(helpContext, /explicit\.dataset\.helpModuleId \|\| base\.moduleId/, "context help honors an explicit documentation owner");
|
|
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.app-main \{[\s\S]*grid-template-rows: 104px 51px minmax\(0, 1fr\);/, "the narrow shell reserves two non-overlapping titlebar rows");
|
|
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.titlebar-context-selectors \{[\s\S]*overflow-x: auto;/, "narrow context selectors remain reachable without covering titlebar actions");
|
|
assert.match(layoutStyles, /@media \(max-width: 600px\)[\s\S]*\.account-pill span \{[\s\S]*display: none;/, "narrow account controls retain the icon while removing collision-prone text");
|
|
assert.match(moduleLoadBoundary, /<DismissibleAlert tone="danger" compact/, "module failures use the compact shared alert");
|
|
assert.match(wysiwygEditor, /editor\.setEditable\(!disabled, false\)/, "mounting and permission changes never emit rich-text changes");
|
|
assert.match(wysiwygEditor, /if \(!isWysiwygDocumentUpdate\(transaction, appendedTransactions\)\) return;/, "rich-text editors ignore non-document update events");
|
|
assert.match(wysiwygEditor, /if \(nextValue === valueRef\.current\) return;/, "rich-text editors do not report identical content as an edit");
|
|
|
|
console.log("Core interface-pattern contracts passed.");
|