mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
fix(replay-ui): read data-* attributes by their markup names
The web runtime now publishes raw markup attribute names, so attrs["step"] read nothing where the markup writes data-step. Three properties went vacuous and exactlyOneStepIsSelected reported false against a UI that was fine. The test also fails if a dataOf key gains no matching attribute, or if an attribute it derives is rendered nowhere.
This commit is contained in:
1 parent
5651adb439
commit
e432af7828
3 files changed
+69
-1
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
// attrs is keyed by the names the markup writes (rawAttributes in
|
||||||
|
// pkg/spec/src/web-runtime.ts), and the goja host the properties run in exposes
|
||||||
|
// attrs and nothing else (internal/verifier/marshal.go): there is no dataset
|
||||||
|
// field to read a camelCased key off, so the spec converts the key itself.
|
||||||
|
export function attributeOf(key: string): string {
|
||||||
|
return `data-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { type AccessibilityElement, type Key, PressKey, Tap, actions, always, extract, from, next, weighted } from "@sanderling/spec";
|
import { type AccessibilityElement, type Key, PressKey, Tap, actions, always, extract, from, next, weighted } from "@sanderling/spec";
|
||||||
import { defaultActions, noUncaughtExceptions } from "@sanderling/spec/defaults";
|
import { defaultActions, noUncaughtExceptions } from "@sanderling/spec/defaults";
|
||||||
|
import { attributeOf } from "./data-attributes";
|
||||||
|
|
||||||
function numberOf(value: string | undefined): number | null {
|
function numberOf(value: string | undefined): number | null {
|
||||||
if (value === undefined || value === "") return null;
|
if (value === undefined || value === "") return null;
|
||||||
@@ -21,7 +22,7 @@ function numberOf(value: string | undefined): number | null {
|
|||||||
|
|
||||||
function dataOf(element: AccessibilityElement | undefined, key: string): string | undefined {
|
function dataOf(element: AccessibilityElement | undefined, key: string): string | undefined {
|
||||||
const attrs = (element as unknown as { attrs?: Record<string, string> } | undefined)?.attrs;
|
const attrs = (element as unknown as { attrs?: Record<string, string> } | undefined)?.attrs;
|
||||||
return attrs ? attrs[key] : undefined;
|
return attrs ? attrs[attributeOf(key)] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The toolbar's own claim about which step is on screen, and how many there are.
|
// The toolbar's own claim about which step is on screen, and how many there are.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { readFileSync, readdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { attributeOf } from "../../sanderling/data-attributes";
|
||||||
|
|
||||||
|
// Bug class: the web runtime keys attrs by the raw markup name, so a spec
|
||||||
|
// reading attrs["stepCount"] gets undefined and every property over it passes
|
||||||
|
// vacuously against a UI that is broken.
|
||||||
|
describe("attributeOf", () => {
|
||||||
|
it("names the attribute the markup writes for each logical key", () => {
|
||||||
|
expect(attributeOf("step")).toBe("data-step");
|
||||||
|
expect(attributeOf("stepCount")).toBe("data-step-count");
|
||||||
|
expect(attributeOf("active")).toBe("data-active");
|
||||||
|
expect(attributeOf("violations")).toBe("data-violations");
|
||||||
|
expect(attributeOf("tabId")).toBe("data-tab-id");
|
||||||
|
expect(attributeOf("violationCount")).toBe("data-violation-count");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const here = new URL(".", import.meta.url).pathname;
|
||||||
|
const specPath = join(here, "../../sanderling/spec.ts");
|
||||||
|
const sourceRoot = join(here, "..");
|
||||||
|
|
||||||
|
function sourceFiles(directory: string): string[] {
|
||||||
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const path = join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) return entry.name === "__tests__" ? [] : sourceFiles(path);
|
||||||
|
return entry.name.endsWith(".tsx") || entry.name.endsWith(".ts") ? [path] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function keysReadByTheSpec(): string[] {
|
||||||
|
const spec = readFileSync(specPath, "utf8");
|
||||||
|
const keys = new Set<string>();
|
||||||
|
for (const match of spec.matchAll(/dataOf\([^,]+,\s*"([^"]+)"\)/g)) keys.add(match[1]);
|
||||||
|
return [...keys];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("the keys the spec reads", () => {
|
||||||
|
const keys = keysReadByTheSpec();
|
||||||
|
|
||||||
|
it("covers every dataOf call site", () => {
|
||||||
|
expect(keys.sort()).toEqual([
|
||||||
|
"active",
|
||||||
|
"step",
|
||||||
|
"stepCount",
|
||||||
|
"tabId",
|
||||||
|
"violationCount",
|
||||||
|
"violations",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names attributes the UI renders", () => {
|
||||||
|
const markup = sourceFiles(sourceRoot)
|
||||||
|
.map((path) => readFileSync(path, "utf8"))
|
||||||
|
.join("\n");
|
||||||
|
const missing = keys.map(attributeOf).filter((attribute) => !markup.includes(`${attribute}=`));
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user