Extract independent MIT Mortar kit and contribution workflow
Mortar contribution checks / Mortar baseline and browser checks (push) Successful in 3m15s

This commit is contained in:
tony committed 2026-10-13 22:16:21 +01:00
commit 43fe76158c
108 files changed
+10081

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, cp, readFile, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkKit, ROOT } from "../scripts/check.mjs";
test("all public exports have valid contracts and previews", async () =>
assert.deepEqual((await checkKit()).findings, []));
test("baseline rejects path escape, missing dependencies, hidden exports and hosted coupling", async () => {
const dir = await mkdtemp(join(tmpdir(), "mortar-baseline-"));
try {
for (const name of [
"src",
"previews",
"manifests",
"package.json",
"baseline-policy.json",
])
await cp(join(ROOT, name), join(dir, name), { recursive: true });
const file = join(dir, "manifests/mortar-button.json");
const c = JSON.parse(await readFile(file, "utf8"));
c.source = "../outside.tsx";
c.contract.dependencies = ["missing"];
await writeFile(file, JSON.stringify(c));
await writeFile(
join(dir, "src/index.ts"),
"export { Surprise } from './primitives.js';\n",
{ flag: "a" },
);
await writeFile(
join(dir, "src/forbidden.ts"),
"fetch('https://example.invalid'); const classes='bg-indigo-600';\n",
);
const result = await checkKit(dir);
assert.equal(result.passed, false);
for (const rule of ["M001", "M002", "M003", "M004", "M005"])
assert.ok(
result.findings.some((f) => f.rule === rule),
rule,
);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
+33
View File
@@ -0,0 +1,33 @@
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
import { readFile } from "node:fs/promises";
const { components } = JSON.parse(
await readFile(new URL("../../catalog.json", import.meta.url), "utf8"),
);
for (const component of components)
for (const theme of ["light", "dark"])
test(`${component.export} ${theme}`, async ({ page }) => {
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.goto(`/?component=${component.export}&theme=${theme}`);
await expect(
page.getByRole("heading", { name: "Mortar playground" }),
).toBeVisible();
await expect(
page.getByRole("status", { name: "Loading preview…" }),
).toHaveCount(0);
await expect(page.locator("[data-mortar-appearance]")).toHaveAttribute(
"data-mortar-appearance",
theme,
);
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21aa"])
.analyze();
expect(
results.violations.map((v) => ({
id: v.id,
nodes: v.nodes.map((n) => n.target),
})),
).toEqual([]);
expect(errors).toEqual([]);
});
+54
View File
@@ -0,0 +1,54 @@
import { test, expect } from "@playwright/test";
// Rendering and axe coverage for every export lives in mortar-fixtures.spec.mjs.
for (const theme of ["light", "dark"]) {
test(`Mortar menu keyboard and escape: ${theme}`, async ({ page }) => {
await page.goto(`/?component=Dropdown&theme=${theme}`);
const button = page.getByRole("button", { name: "Actions", exact: true });
await button.focus();
await page.keyboard.press("ArrowDown");
await expect(page.getByRole("menu")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(button).toBeFocused();
});
test(`Mortar modal traps focus and restores it: ${theme}`, async ({
page,
}) => {
await page.goto(`/?component=Modal&theme=${theme}`);
const button = page.getByRole("button", { name: "Open dialog" });
await button.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toHaveAttribute("aria-modal", "true");
for (let i = 0; i < 5; i++) {
await page.keyboard.press("Tab");
expect(
await dialog.evaluate((e) => e.contains(document.activeElement)),
).toBe(true);
}
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
await expect(button).toBeFocused();
});
test(`Mortar controlled choice fields: ${theme}`, async ({ page }) => {
await page.goto(`/?component=Checkbox&theme=${theme}`);
const checkbox = page.getByRole("checkbox", {
name: "Email notifications",
});
await expect(checkbox).not.toBeChecked();
await checkbox.click();
await expect(checkbox).toBeChecked();
await page.goto(`/?component=Switch&theme=${theme}`);
const toggle = page.getByRole("switch", { name: "Automatic updates" });
await expect(toggle).not.toBeChecked();
await toggle.click();
await expect(toggle).toBeChecked();
await page.goto(`/?component=RadioGroup&theme=${theme}`);
const radio = page.getByRole("radio", { name: /Advanced/ });
await radio.click();
await expect(radio).toBeChecked();
await page.goto(`/?component=Select&theme=${theme}`);
const select = page.getByLabel("Environment", { exact: true });
await select.selectOption("second");
await expect(select).toHaveValue("second");
});
}
+63
View File
@@ -0,0 +1,63 @@
import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { AppShell, Input, DataTable, CodeBlock } from "../dist/index.js";
test("Mortar shell supplies page landmarks and labelled navigation", () => {
const html = renderToStaticMarkup(
React.createElement(
AppShell,
{
brand: "Example",
navigation: [{ label: "Overview", href: "#overview", current: true }],
},
"Content",
),
);
assert.match(html, /aria-current="page"/);
assert.match(html, /<main id="m-content"/);
assert.match(html, /Skip to content/);
});
test("Mortar fields keep unique labels and external error descriptions", () => {
const html = renderToStaticMarkup(
React.createElement(
React.Fragment,
{},
React.createElement(Input, {
label: "Email",
error: "Invalid email",
"aria-describedby": "external",
}),
React.createElement(Input, { label: "Name" }),
),
);
const ids = [...html.matchAll(/<input[^>]*\sid="([^"]+)"/g)].map((m) => m[1]);
assert.equal(ids.length, 2);
assert.notEqual(ids[0], ids[1]);
assert.ok(html.includes("external " + ids[0] + "-message"));
assert.ok(html.includes('for="' + ids[1] + '"'));
});
test("Mortar table separates loading, error and empty states and keeps row headers", () => {
const props = {
caption: "Records",
columns: [{ key: "name", label: "Name", render: (row) => row.name }],
rowLabel: (row) => row.name,
rows: [{ id: "1", name: "Example" }],
};
const render = (extra) =>
renderToStaticMarkup(
React.createElement(DataTable, { ...props, ...extra }),
);
assert.match(render({}), /scope="row"/);
assert.match(render({}), /tabindex="0"/);
assert.match(render({ rows: [] }), /No records/);
assert.match(render({ loading: true }), /role="status"/);
assert.match(render({ error: "Unavailable" }), /role="alert"/);
});
test("Mortar source inspection never executes submitted markup", () => {
const html = renderToStaticMarkup(
React.createElement(CodeBlock, { code: "<script>alert(1)</script>" }),
);
assert.ok(!html.includes("<script>"));
assert.match(html, /&lt;script&gt;/);
});