diff --git a/docs/design.md b/docs/design.md
index 98c72bb..de03d1d 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -214,10 +214,10 @@ boundary dash, hairlines for the rest. It never goes on a page with a data view
The header scrolls away with the page, so `--header-h` (what sticky content must clear) is 0.
-Product surfaces go in `product`. The matrix (`MatrixTable`, `StatusCell`, `ObservationDetail`)
+Product surfaces go in `product`. The matrix (`MatrixTable`, `StatusCell`, `ClassIcon`)
keeps its styles there through `
diff --git a/src/components/Matrix.svelte b/src/components/Matrix.svelte
index d8d0c7d..209ad31 100644
--- a/src/components/Matrix.svelte
+++ b/src/components/Matrix.svelte
@@ -1,16 +1,11 @@
-
-
-
-
diff --git a/src/components/MatrixPage.astro b/src/components/MatrixPage.astro
index a80755d..2777643 100644
--- a/src/components/MatrixPage.astro
+++ b/src/components/MatrixPage.astro
@@ -20,6 +20,7 @@ import {
import { getDataset } from "@/lib/data";
import { CATEGORY_LABEL, PARTY_LABEL, STATUS_ORDER } from "@/lib/labels";
import { latestScan, passiveChecks } from "@/lib/scanner";
+import ClassIcon from "./ClassIcon.astro";
import Matrix from "./Matrix.svelte";
import MatrixTable from "./MatrixTable.astro";
import StatusCell from "./StatusCell.astro";
@@ -69,7 +70,8 @@ const classWords = (counts: { value: string; count: number }[], label: RecordWho supports what
The public MCP servers we list, against what the MCP Authorization specification requires
- of servers and their authorization servers. Each cell opens the observation behind it.
+ of servers and their authorization servers. Each cell links to the observation behind it on
+ the server's page.
@@ -194,7 +196,9 @@ const classWords = (counts: { value: string; count: number }[], label: RecordAS name the party the feature is observed on. The
nightly scanner reads {scannerChecks} of these requirements from
- public metadata; "scan" next to a server's name opens what it asked.
+ public metadata; the trace mark
+
+ next to a server's name opens what it asked.
Rows are alphabetical. Nothing here is scored or ranked.
@@ -204,7 +208,10 @@ const classWords = (counts: { value: string; count: number }[], label: Record (
<>
{i > 0 && " · "}
- {ACCESS_LABEL[v].toLowerCase()}
+
+
+ {ACCESS_LABEL[v].toLowerCase()}
+
>
))}
: who can connect, as the server page's evidence says.
@@ -216,11 +223,14 @@ const classWords = (counts: { value: string; count: number }[], label: Record (
<>
{i > 0 && " · "}
- {AUTH_LABEL[v].toLowerCase()}
+
+
+ {AUTH_LABEL[v].toLowerCase()}
+
>
))}
- : how the endpoint authenticates clients. A dashed chip is a class nobody has stated
- yet. Both come from the evidence on the server page. API-key and unauthenticated
+ : how the endpoint authenticates clients. A faint mark after a server's name is a class
+ nobody has stated yet. Both come from the evidence on the server page. API-key and unauthenticated
servers are not scanned.
@@ -382,6 +392,24 @@ const classWords = (counts: { value: string; count: number }[], label: Record id.split(".").slice(2).join(".") || id;
- No observation on record for this server and feature under revision {cell.revision}.
- {cell.status === "not-applicable"
- ? " The server has no HTTP transport, so the feature does not apply."
- : " The status stays unknown until someone records one."}
-
- )
- }
-
-
-
diff --git a/src/pages/accessibility.astro b/src/pages/accessibility.astro
index 8f83d3d..6dfb114 100644
--- a/src/pages/accessibility.astro
+++ b/src/pages/accessibility.astro
@@ -47,8 +47,8 @@ const motions = [
},
{
where: "Matrix",
- what: "The row and column under your pointer or focus take a tint. A cell opens a dialog that fades in and rises 8 px.",
- reduced: "The tint and the dialog appear at once.",
+ what: "The row and column under your pointer or focus take a tint.",
+ reduced: "The tint appears at once.",
},
{
where: "Scanner report",
diff --git a/src/pages/mcp/servers/[id].astro b/src/pages/mcp/servers/[id].astro
index 981bb62..f5cdf54 100644
--- a/src/pages/mcp/servers/[id].astro
+++ b/src/pages/mcp/servers/[id].astro
@@ -273,7 +273,7 @@ const classes = [
const entry = featureInRevision(feature, revision.id);
const obs = cell.observation;
return (
-
+
{feature.name}
@@ -521,6 +521,13 @@ const classes = [
.features tbody tr:hover > * {
background: var(--signal-soft);
}
+ /* a matrix cell links here: its row takes the tint and keeps clear of the top edge */
+ .features tbody tr:target > * {
+ background: var(--signal-soft);
+ }
+ .features tbody tr {
+ scroll-margin-top: var(--space-6);
+ }
.features th[scope="row"] {
font-size: var(--small);
font-weight: 500;
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png
index 0002c68..af493ef 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png and b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-dark.png differ
diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png
index 2b9684c..9d2a9b2 100644
Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png and b/tests/e2e/__screenshots__/matrix.spec.ts/status-shapes-light.png differ
diff --git a/tests/e2e/matrix.spec.ts b/tests/e2e/matrix.spec.ts
index 18b3ad6..b242d88 100644
--- a/tests/e2e/matrix.spec.ts
+++ b/tests/e2e/matrix.spec.ts
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
import { currentCell, currentRevision, matrixFor } from "../../src/lib/atlas";
import { ACCESS_ORDER, AUTH_ORDER, accessOf, authOf, countBy } from "../../src/lib/classes";
import { getDataset } from "../../src/lib/data";
+import { METHOD_LABEL } from "../../src/lib/labels";
import { latestScan } from "../../src/lib/scanner";
import { expectEndStateUnderReducedMotion, expectNoSeriousA11yViolations } from "./helpers";
@@ -46,29 +47,26 @@ test.describe("matrix page (US1)", () => {
expect(statuses.every((s) => s && STATUSES.includes(s))).toBe(true);
});
- test("cell detail shows the observation and returns focus on close (FR-002)", async ({
- page,
- }) => {
+ test("a cell links to its observation on the server page (FR-002)", async ({ page }) => {
test.skip(!withUnsupported, "no server with an unsupported observation in the seed data");
await page.goto(`/mcp/${dcrGroup}`);
await expect(page.locator("#matrix[data-enhanced]")).toBeAttached();
- const summary = page.locator(
- `td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] summary`,
+ const cell = page.locator(
+ `td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] a.cell`,
);
- await summary.click();
- const dialog = page.locator("dialog.cell-dialog");
- await expect(dialog).toBeVisible();
- // focus lands on the dialog's heading (016 WP4) and goes back to the cell on close
- await expect(dialog.locator("#cell-dialog-title")).toBeFocused();
- await expect(dialog).toContainText("Unsupported");
+ await expect(cell).toHaveAttribute("href", `/mcp/servers/${withUnsupported?.id}/#mcp.as.dcr`);
+ await cell.click();
+ await expect(page).toHaveURL(
+ new RegExp(`/mcp/servers/${withUnsupported?.id}/#mcp\\.as\\.dcr$`),
+ );
+ const row = page.locator("tr:target");
+ await expect(row).toHaveAttribute("data-feature", "mcp.as.dcr");
+ await expect(row).toBeInViewport();
+ await expect(row).toContainText("Unsupported");
const winning = matrix.cells.get(`${withUnsupported?.id}|mcp.as.dcr`)?.observation;
- await expect(dialog).toContainText(winning?.method ?? "vendor-claim");
- await expect(dialog).toContainText(winning?.observedAt.slice(0, 10) ?? "");
- await expect(dialog).toContainText(winning?.observedBy ?? "");
- await expect(dialog.locator("dd a[href^='https://']")).toHaveCount(1);
- await page.keyboard.press("Escape");
- await expect(dialog).toBeHidden();
- await expect(summary).toBeFocused();
+ await expect(row).toContainText(METHOD_LABEL[winning?.method ?? "vendor-claim"]);
+ await expect(row).toContainText(winning?.observedAt.slice(0, 10) ?? "");
+ await expect(row).toContainText(winning?.observedBy ?? "");
});
test("a server without observations reads unknown everywhere, never unsupported (SC-006)", async ({
@@ -79,7 +77,7 @@ test.describe("matrix page (US1)", () => {
const row = page.locator(`#matrix tbody tr[data-server="${allUnknown?.id}"]`);
await expect(row.locator("td[data-status='unknown']")).toHaveCount(matrix.columns.length);
await expect(row.locator("td[data-status='unsupported']")).toHaveCount(0);
- await expect(row.locator("td").first().locator("summary .status-unknown")).toBeVisible();
+ await expect(row.locator("td").first().locator("a.cell .status-unknown")).toBeVisible();
});
test("filter state in the URL restores the filtered view (FR-008)", async ({ page }) => {
@@ -197,21 +195,17 @@ test.describe("matrix page (US1)", () => {
await expect(page.locator("#matrix [data-grade]")).toHaveCount(0);
});
- test("keyboard-only walkthrough opens and closes a cell", async ({ page }) => {
+ test("keyboard-only walkthrough follows a cell", async ({ page }) => {
test.skip(!withUnsupported, "no server with an unsupported observation in the seed data");
await page.goto(`/mcp/${dcrGroup}`);
await expect(page.locator("#matrix[data-enhanced]")).toBeAttached();
- const summary = page.locator(
- `td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] summary`,
+ const cell = page.locator(
+ `td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] a.cell`,
);
- await summary.focus();
+ await cell.focus();
+ await expect(cell).toBeFocused();
await page.keyboard.press("Enter");
- const dialog = page.locator("dialog.cell-dialog");
- await expect(dialog).toBeVisible();
- await page.keyboard.press("Tab");
- await page.keyboard.press("Escape");
- await expect(dialog).toBeHidden();
- await expect(summary).toBeFocused();
+ await expect(page.locator("tr:target")).toHaveAttribute("data-feature", "mcp.as.dcr");
});
test("has no horizontal page scroll at phone width (constitution V)", async ({ page }) => {
@@ -304,23 +298,6 @@ test.describe("matrix page (US1)", () => {
await expect(legend).toHaveScreenshot("status-shapes.png");
});
- test("cell detail opens without motion under reduced motion (M10)", async ({ page }) => {
- test.skip(!withUnsupported, "no server with an unsupported observation in the seed data");
- await page.emulateMedia({ reducedMotion: "reduce" });
- await page.goto(`/mcp/${dcrGroup}`);
- await expect(page.locator("#matrix[data-enhanced]")).toBeAttached();
- await page
- .locator(`td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] summary`)
- .click();
- const dialog = page.locator("dialog.cell-dialog");
- await expect(dialog).toBeVisible();
- const running = await dialog.evaluate(
- (el) => el.getAnimations({ subtree: true }).filter((a) => a.playState === "running").length,
- );
- expect(running).toBe(0);
- expect(await dialog.evaluate((el) => getComputedStyle(el).opacity)).toBe("1");
- });
-
test("the crosshair shows at once under reduced motion (M9)", async ({ page }) => {
await page.goto("/mcp/");
await expectEndStateUnderReducedMotion(page, "#matrix tbody tr[data-server]");
@@ -355,20 +332,20 @@ test.describe("matrix page (US1)", () => {
test.describe("matrix page without JavaScript", () => {
test.use({ javaScriptEnabled: false });
- test("renders the full matrix and native cell details", async ({ page }) => {
+ test("renders the full matrix and cells that link to their observation", async ({ page }) => {
test.skip(!withUnsupported, "no server with an unsupported observation in the seed data");
await page.goto(`/mcp/?level=MUST${dcrGroup}`);
await expect(page.locator("#matrix tbody tr[data-server]")).toHaveCount(matrix.rows.length);
await expect(page.locator("#matrix tbody tr[data-server]:not([hidden])")).toHaveCount(
matrix.rows.length,
);
- const details = page.locator(
- `td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] details`,
- );
- await details.locator("summary").click();
- await expect(details).toHaveAttribute("open", "");
+ await page
+ .locator(`td[data-server="${withUnsupported?.id}"][data-feature="mcp.as.dcr"] a.cell`)
+ .click();
+ const row = page.locator("tr:target");
+ await expect(row).toHaveAttribute("data-feature", "mcp.as.dcr");
const winning = matrix.cells.get(`${withUnsupported?.id}|mcp.as.dcr`)?.observation;
- await expect(details.locator("[data-detail]")).toContainText(winning?.method ?? "vendor-claim");
+ await expect(row).toContainText(METHOD_LABEL[winning?.method ?? "vendor-claim"]);
});
test("on a phone, the group picker works through plain anchors (follow-up 5)", async ({
diff --git a/tests/e2e/standards.spec.ts b/tests/e2e/standards.spec.ts
index e0c6111..2417e0e 100644
--- a/tests/e2e/standards.spec.ts
+++ b/tests/e2e/standards.spec.ts
@@ -169,10 +169,15 @@ test.describe("standards map (US1)", () => {
await expect(page.locator("#spec-list li[data-spec][data-recent]")).toHaveCount(5);
const narrow = (page.viewportSize()?.width ?? 1280) < 640;
if (!narrow) {
- const faded = page.locator("#map a[data-spec]:not([data-recent])").first();
- expect(await faded.evaluate((el) => getComputedStyle(el).opacity)).toBe("0.2");
- const kept = page.locator("#map a[data-spec][data-recent]").first();
- expect(await kept.evaluate((el) => getComputedStyle(el).opacity)).toBe("1");
+ // the fade is a transition, so wait for it to settle rather than read it mid-way
+ await expect(page.locator("#map a[data-spec]:not([data-recent])").first()).toHaveCSS(
+ "opacity",
+ "0.2",
+ );
+ await expect(page.locator("#map a[data-spec][data-recent]").first()).toHaveCSS(
+ "opacity",
+ "1",
+ );
}
await page.locator("#map-filters input[value='recent']").uncheck();
await expect(page).not.toHaveURL(/view=/);