152 lines
5.0 KiB
JavaScript
152 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, resolve } from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const read = (path) => readFileSync(resolve(webRoot, path), "utf8");
|
|
const require = createRequire(import.meta.url);
|
|
|
|
function loadTsxComponent(path) {
|
|
const typescript = require("typescript");
|
|
const output = typescript.transpileModule(read(path), {
|
|
compilerOptions: {
|
|
esModuleInterop: true,
|
|
jsx: typescript.JsxEmit.ReactJSX,
|
|
module: typescript.ModuleKind.CommonJS,
|
|
target: typescript.ScriptTarget.ES2020,
|
|
},
|
|
}).outputText;
|
|
const loadedModule = { exports: {} };
|
|
new Function("require", "module", "exports", output)(
|
|
require,
|
|
loadedModule,
|
|
loadedModule.exports,
|
|
);
|
|
return loadedModule.exports;
|
|
}
|
|
|
|
const migratedPresentation = [
|
|
"components/Layout.tsx",
|
|
"components/PortalUi.tsx",
|
|
"pages/index.tsx",
|
|
"pages/jobs/[jobId].tsx",
|
|
];
|
|
|
|
test("migrated shell and job pages use reusable classes without inline presentation", () => {
|
|
for (const path of migratedPresentation) {
|
|
const source = read(path);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/\bstyle\s*=/,
|
|
`${path} must not use inline style props`,
|
|
);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/#[\da-f]{3,8}\b/i,
|
|
`${path} must not define visual color literals`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("the authenticated shell keeps organization and navigation context accessible", () => {
|
|
const layout = read("components/Layout.tsx");
|
|
assert.match(layout, /Active organization/);
|
|
assert.match(layout, /activeOrg\.role/);
|
|
assert.match(layout, /aria-current=/);
|
|
assert.match(layout, /Skip to main content/);
|
|
assert.match(layout, /aria-label="Primary navigation"/);
|
|
assert.match(layout, /src="\/um-trace-mark\.png"/);
|
|
assert.doesNotMatch(layout, />\s*UL\s*</);
|
|
});
|
|
|
|
test("the portal uses the cropped UM foreground without nested icon padding", () => {
|
|
const layout = read("components/Layout.tsx");
|
|
const css = read("styles/global.css");
|
|
assert.match(layout, /src="\/um-trace-mark\.png"/);
|
|
assert.doesNotMatch(layout, /um-trace-icon\.svg/);
|
|
assert.match(css, /\.ul-shell__brand-mark\s*\{[^}]*object-fit:\s*contain;/s);
|
|
assert.match(css, /\.ul-shell__brand-mark\s*\{[^}]*padding:\s*0\.375rem;/s);
|
|
});
|
|
|
|
test("status presentation always combines a readable label with an icon", () => {
|
|
const components = read("components/PortalUi.tsx");
|
|
for (const status of [
|
|
"OPEN",
|
|
"IN_PROGRESS",
|
|
"COMPLETED",
|
|
"CANCELLED",
|
|
"LIVE",
|
|
"OFFLINE",
|
|
"ERROR",
|
|
]) {
|
|
assert.match(
|
|
components,
|
|
new RegExp(`${status}: \\{ icon: .+, label: .+, tone:`),
|
|
);
|
|
}
|
|
assert.match(components, /ul-status-badge__icon/);
|
|
assert.match(components, /aria-hidden="true"/);
|
|
});
|
|
|
|
test("status and error-state components render accessible text and semantics", () => {
|
|
const React = require("react");
|
|
const { renderToStaticMarkup } = require("react-dom/server");
|
|
const { PageState, StatusBadge } = loadTsxComponent(
|
|
"components/PortalUi.tsx",
|
|
);
|
|
|
|
const status = renderToStaticMarkup(
|
|
React.createElement(StatusBadge, { status: "IN_PROGRESS" }),
|
|
);
|
|
assert.match(status, />In progress</);
|
|
assert.match(status, /ul-status-badge--warning/);
|
|
assert.match(status, /aria-hidden="true"/);
|
|
|
|
const error = renderToStaticMarkup(
|
|
React.createElement(PageState, {
|
|
kind: "error",
|
|
title: "Jobs could not be loaded",
|
|
}),
|
|
);
|
|
assert.match(error, /role="alert"/);
|
|
assert.match(error, /Jobs could not be loaded/);
|
|
});
|
|
|
|
test("responsive, focus, touch-target, and reduced-motion rules are present", () => {
|
|
const css = read("styles/global.css");
|
|
assert.ok(css.startsWith('@import "./tokens.css";'));
|
|
for (const token of [
|
|
"--ul-color-canvas",
|
|
"--ul-color-primary",
|
|
"--ul-color-map",
|
|
"--ul-touch-minimum",
|
|
"--ul-type-family-sans",
|
|
]) {
|
|
assert.match(css, new RegExp(`var\\(${token}\\)`));
|
|
}
|
|
assert.match(css, /:focus-visible/);
|
|
assert.match(css, /min-height:\s*var\(--ul-touch-minimum\)/);
|
|
assert.match(css, /@media \(max-width: 1200px\)/);
|
|
assert.match(css, /@media \(max-width: 900px\)/);
|
|
assert.match(css, /@media \(max-width: 600px\)/);
|
|
assert.match(css, /@media \(prefers-reduced-motion: reduce\)/);
|
|
});
|
|
|
|
test("job pages preserve organization-scoped APIs, streaming, and provider-neutral map usage", () => {
|
|
const jobs = read("pages/index.tsx");
|
|
const detail = read("pages/jobs/[jobId].tsx");
|
|
const dispatcher = read("components/map/JobMap.tsx");
|
|
const stream = read("lib/use-job-stream.ts");
|
|
|
|
assert.match(jobs, /\/api\/orgs\/\$\{activeOrg\.org\.id\}\/jobs/);
|
|
assert.match(detail, /\/api\/orgs\/\$\{orgId\}\/jobs\/\$\{jobId\}/);
|
|
assert.match(detail, /<JobMap/);
|
|
assert.doesNotMatch(detail, /EsriJobMap/);
|
|
assert.match(dispatcher, /dynamic\(\(\) => import\('\.\/esri\/EsriJobMap'\)/);
|
|
assert.match(stream, /channel: `job:\$\{jobId\}`/);
|
|
assert.match(stream, /return connectionState/);
|
|
});
|