forked from SmartDropLabs/smartdrop-frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtable-accessibility.spec.ts
More file actions
133 lines (111 loc) · 4.73 KB
/
Copy pathtable-accessibility.spec.ts
File metadata and controls
133 lines (111 loc) · 4.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { test, expect, type Page } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
// Extends the pattern in contrast-audit.spec.ts to cover #86: aria-sort
// semantics and aria-live announcements on the Leaderboard/History tables.
function leaderboardEntry(i: number) {
return {
address: `GLEADER${String(i).padStart(3, "0")}${"A".repeat(45)}`.slice(0, 56),
totalCredits: 1000 - i,
totalStake: 500 - i,
boostUtilization: 10,
};
}
async function mockLeaderboardApi(page: Page, total: number): Promise<void> {
await page.route("**/__mock-leaderboard-api**", async (route) => {
const url = new URL(route.request().url());
const offset = Number(url.searchParams.get("offset") ?? 0);
const limit = Number(url.searchParams.get("limit") ?? 10);
const sort = url.searchParams.get("sort") ?? "credits";
const all = Array.from({ length: total }, (_, i) => leaderboardEntry(i));
all.sort((a, b) =>
sort === "credits"
? b.totalCredits - a.totalCredits
: b.totalStake - a.totalStake,
);
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({
entries: all.slice(offset, offset + limit),
total,
}),
});
});
}
test.describe("Leaderboard aria-sort semantics (#86)", () => {
test("aria-sort on Credits/Stake headers matches the active sort column, for both sort options", async ({
page,
}) => {
await mockLeaderboardApi(page, 25);
await page.goto("/leaderboard");
const creditsHeader = page.getByRole("columnheader", { name: /credits/i });
const stakeHeader = page.getByRole("columnheader", { name: /stake/i });
await expect(creditsHeader).toHaveAttribute("aria-sort", "descending");
await expect(stakeHeader).toHaveAttribute("aria-sort", "none");
await page.getByRole("button", { name: "Stake" }).click();
await expect(creditsHeader).toHaveAttribute("aria-sort", "none");
await expect(stakeHeader).toHaveAttribute("aria-sort", "descending");
});
test("the sort Select is programmatically associated with the table via aria-controls", async ({
page,
}) => {
await mockLeaderboardApi(page, 25);
await page.goto("/leaderboard");
const select = page.getByRole("combobox");
const controlsId = await select.getAttribute("aria-controls");
expect(controlsId).toBeTruthy();
const table = page.locator(`#${controlsId}`);
await expect(table).toHaveCount(1);
await expect(table).toHaveJSProperty("tagName", "TABLE");
});
test("changing the Select keeps aria-sort in sync (both controls drive the same state)", async ({
page,
}) => {
await mockLeaderboardApi(page, 25);
await page.goto("/leaderboard");
await page.getByRole("combobox").selectOption("stake");
await expect(
page.getByRole("columnheader", { name: /stake/i }),
).toHaveAttribute("aria-sort", "descending");
});
test("has no axe-core violations with a populated, sorted table", async ({
page,
}) => {
await mockLeaderboardApi(page, 25);
await page.goto("/leaderboard");
await page.getByRole("button", { name: "Stake" }).click();
await expect(
page.getByRole("columnheader", { name: /stake/i }),
).toHaveAttribute("aria-sort", "descending");
// Scoped to WCAG conformance rules, matching contrast-audit.spec.ts's
// own approach of targeting a specific concern rather than asserting
// zero violations against axe's full best-practice ruleset (which
// includes pre-existing, unrelated gaps like page-has-heading-one —
// out of scope for this table-sort/live-region issue).
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(results.violations).toEqual([]);
});
});
test.describe("Leaderboard/History live-region announcements (#86)", () => {
test("leaderboard exposes a polite, atomic live region announcing the loaded range", async ({
page,
}) => {
await mockLeaderboardApi(page, 25);
await page.goto("/leaderboard");
const status = page.getByRole("status");
await expect(status).toHaveAttribute("aria-live", "polite");
await expect(status).toHaveAttribute("aria-atomic", "true");
await expect(status).toHaveText(/showing rank 1-10 of 25/i);
});
test("history exposes a live region (present even with no sort UI, per issue scope)", async ({
page,
}) => {
await page.goto("/history");
// Not connected: nothing to announce yet, but the region must still
// exist in the accessibility tree so a screen reader is primed for
// it once the wallet connects and history loads.
const status = page.getByRole("status");
await expect(status).toHaveAttribute("aria-live", "polite");
});
});