Skip to content

Commit 5565138

Browse files
committed
Add ActionMenu strict trigger visuals
1 parent e0b27f0 commit 5565138

7 files changed

Lines changed: 253 additions & 107 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { expect, test, type Locator, type Page } from "@playwright/test";
2+
import { frameworkPanel, styledSection, waitForComparisonRouteReady } from "./comparison-page";
3+
import { clearPointer, expectExactScreenshotPair, pinComparisonTheme } from "./visual-diff";
4+
5+
function actionMenuQuery(params: Record<string, string | boolean> = {}) {
6+
const search = new URLSearchParams();
7+
for (const [key, value] of Object.entries(params)) {
8+
if (value !== "" && value !== false) {
9+
search.set(key, String(value));
10+
}
11+
}
12+
13+
const query = search.toString();
14+
return query ? `?${query}` : "";
15+
}
16+
17+
async function actionMenuFixtures(page: Page, params: Record<string, string | boolean> = {}) {
18+
await pinComparisonTheme(page, "dark");
19+
await page.goto(`/components/actionmenu/${actionMenuQuery(params)}`);
20+
await waitForComparisonRouteReady(page);
21+
await clearPointer(page);
22+
23+
const section = await styledSection(page);
24+
const reactPanel = await frameworkPanel(section, "React Spectrum stack");
25+
const solidPanel = await frameworkPanel(section, "Solidaria stack");
26+
const reactTrigger = reactPanel.getByRole("button", { name: "More actions" });
27+
const solidTrigger = solidPanel.getByRole("button", { name: "More actions" });
28+
29+
await expect(reactTrigger).toBeVisible();
30+
await expect(solidTrigger).toBeVisible();
31+
32+
return { reactTrigger, solidTrigger };
33+
}
34+
35+
async function actionMenuTriggerContract(trigger: Locator) {
36+
return trigger.evaluate((element) => {
37+
function round(value: number | undefined | null) {
38+
return value == null ? null : Number(value.toFixed(4));
39+
}
40+
41+
function styleMap(node: Element | null, keys: readonly string[]) {
42+
if (!(node instanceof HTMLElement) && !(node instanceof SVGElement)) {
43+
return null;
44+
}
45+
46+
const styles = window.getComputedStyle(node);
47+
return Object.fromEntries(keys.map((key) => [key, styles.getPropertyValue(key)]));
48+
}
49+
50+
function rect(node: Element | null) {
51+
if (!(node instanceof HTMLElement) && !(node instanceof SVGElement)) {
52+
return null;
53+
}
54+
55+
const box = node.getBoundingClientRect();
56+
return {
57+
width: round(box.width),
58+
height: round(box.height),
59+
};
60+
}
61+
62+
const icon = element.querySelector("svg");
63+
64+
return {
65+
tag: element.tagName,
66+
ariaExpanded: element.getAttribute("aria-expanded"),
67+
disabled: element.hasAttribute("disabled"),
68+
style: styleMap(element, [
69+
"display",
70+
"align-items",
71+
"justify-content",
72+
"box-sizing",
73+
"width",
74+
"height",
75+
"padding-top",
76+
"padding-right",
77+
"padding-bottom",
78+
"padding-left",
79+
"border-radius",
80+
"border-style",
81+
"background-color",
82+
"color",
83+
"font-family",
84+
"font-size",
85+
"line-height",
86+
"outline-color",
87+
"outline-style",
88+
"outline-width",
89+
]),
90+
rect: rect(element),
91+
icon: {
92+
tag: icon?.tagName ?? null,
93+
style: styleMap(icon, ["display", "width", "height", "color", "fill"]),
94+
rect: rect(icon),
95+
},
96+
};
97+
});
98+
}
99+
100+
test.describe("comparison ActionMenu visual parity", () => {
101+
test("ActionMenu default trigger is pixel-identical", async ({ page }) => {
102+
const { reactTrigger, solidTrigger } = await actionMenuFixtures(page);
103+
104+
await expectExactScreenshotPair(page, reactTrigger, solidTrigger, "ActionMenu default trigger");
105+
});
106+
107+
test("ActionMenu trigger computed styles match React Spectrum across viewer axes", async ({
108+
page,
109+
}) => {
110+
for (const params of [
111+
{},
112+
{ size: "XS" },
113+
{ size: "XL" },
114+
{ isQuiet: true },
115+
{ isDisabled: true },
116+
] as const) {
117+
const { reactTrigger, solidTrigger } = await actionMenuFixtures(page, params);
118+
119+
await expect(actionMenuTriggerContract(solidTrigger)).resolves.toEqual(
120+
await actionMenuTriggerContract(reactTrigger),
121+
);
122+
}
123+
});
124+
});

0 commit comments

Comments
 (0)