|
1 | 1 | import { test, expect, type Page } from "@playwright/test"; |
2 | 2 | import { Privilege } from "@/proxy"; |
| 3 | +import { createEvent, deleteIfExists } from "../global-setup"; |
3 | 4 |
|
4 | 5 | test.describe("[no login]", () => { |
5 | 6 | test("should not be navigable", async ({ page }) => { |
@@ -64,10 +65,6 @@ test.describe("[mixologist]", () => { |
64 | 65 | }); |
65 | 66 |
|
66 | 67 | test.describe("[admin]", () => { |
67 | | - // These tests mutate the shared "test-event" fixture (item selection, mode); |
68 | | - // run them in order rather than in parallel to avoid clobbering each other. |
69 | | - test.describe.configure({ mode: "serial" }); |
70 | | - |
71 | 68 | test("should be navigable to an existing event", async ({ |
72 | 69 | page, |
73 | 70 | context, |
@@ -132,41 +129,69 @@ test.describe("[admin]", () => { |
132 | 129 | test("should not be able to select more than 9 menu items + navigate to smoothie", async ({ |
133 | 130 | page, |
134 | 131 | context, |
135 | | - }) => { |
136 | | - await context.addCookies([ |
137 | | - { |
138 | | - name: "privilege", |
139 | | - value: Privilege.ADMIN, |
140 | | - url: "http://localhost:3000", |
141 | | - }, |
142 | | - ]); |
143 | | - await context.setExtraHTTPHeaders({ |
144 | | - Authorization: `Basic ${btoa(process.env.ADMIN_LOGIN || ":")}`, |
145 | | - }); |
146 | | - |
147 | | - await page.goto("http://localhost:3000/event/test-event"); |
148 | | - |
149 | | - await page.waitForTimeout(2000); |
150 | | - |
151 | | - // TestEvent starts with 1 item selected (Espresso); select 9 more unselected |
152 | | - // items to reach the 10-item cap. Scoped to the literal aria-pressed="false" |
153 | | - // attribute (not the role=button pressed filter) — Chromium's accessibility |
154 | | - // tree reports pressed:false by default for any plain button, which would |
155 | | - // otherwise also match the header's "Log out" button and toast dismiss buttons. |
156 | | - const unselectedItem = page.locator('button[aria-pressed="false"]'); |
157 | | - for (let i = 0; i < 9; i++) { |
| 132 | + }, testInfo) => { |
| 133 | + // This test mutates item selection and mode, unlike its siblings which only |
| 134 | + // read "test-event" — give it a private event (keyed by parallelIndex, with |
| 135 | + // its own display name) so it never clobbers the shared fixture other spec |
| 136 | + // files depend on, and doesn't produce a duplicate "TestEvent" heading on |
| 137 | + // the home page while other tests are concurrently asserting against it. |
| 138 | + const slug = `test-event-menu-cap-${testInfo.parallelIndex}`; |
| 139 | + // Event names are capped at 20 chars by the API (src/app/api/event/route.ts). |
| 140 | + // Must not contain "TestEvent" as a substring — other tests query |
| 141 | + // getByRole(..., { name: "TestEvent" }) without exact:true, which matches |
| 142 | + // on substring, so any name merely starting with "TestEvent" still collides. |
| 143 | + const name = `MenuCapEvent${testInfo.parallelIndex}`; |
| 144 | + const baseURL = testInfo.project.use.baseURL || "http://localhost:3000"; |
| 145 | + await deleteIfExists(baseURL, slug); |
| 146 | + const response = await createEvent(baseURL, slug, name); |
| 147 | + expect(response.status).toBe(201); |
| 148 | + |
| 149 | + try { |
| 150 | + await context.addCookies([ |
| 151 | + { |
| 152 | + name: "privilege", |
| 153 | + value: Privilege.ADMIN, |
| 154 | + url: "http://localhost:3000", |
| 155 | + }, |
| 156 | + ]); |
| 157 | + await context.setExtraHTTPHeaders({ |
| 158 | + Authorization: `Basic ${btoa(process.env.ADMIN_LOGIN || ":")}`, |
| 159 | + }); |
| 160 | + |
| 161 | + await page.goto(`http://localhost:3000/event/${slug}`); |
| 162 | + |
| 163 | + // Wait for the freshly-created event's menu to actually be rendered |
| 164 | + // (Espresso pre-selected) rather than a fixed sleep, since a brand-new |
| 165 | + // event's Sync data may take longer to propagate under concurrent load. |
| 166 | + await expect( |
| 167 | + page.getByRole("button", { name: "Espresso Strong black coffee" }), |
| 168 | + ).toHaveAttribute("aria-pressed", "true"); |
| 169 | + |
| 170 | + // TestEvent starts with 1 item selected (Espresso); select 9 more unselected |
| 171 | + // items to reach the 10-item cap. Scoped to the literal aria-pressed="false" |
| 172 | + // attribute (not the role=button pressed filter) — Chromium's accessibility |
| 173 | + // tree reports pressed:false by default for any plain button, which would |
| 174 | + // otherwise also match the header's "Log out" button and toast dismiss buttons. |
| 175 | + const unselectedItem = page.locator('button[aria-pressed="false"]'); |
| 176 | + for (let i = 0; i < 9; i++) { |
| 177 | + await unselectedItem.first().click(); |
| 178 | + // Wait for this click's selection save to land before firing the |
| 179 | + // next — the save is a fire-and-forget PUT, and rapid unawaited |
| 180 | + // requests can complete out of order under latency, regressing the |
| 181 | + // count if a later click's save is overtaken by an earlier one. |
| 182 | + await expect(page.getByText(`${i + 2} of 10 items selected`)).toBeVisible(); |
| 183 | + } |
| 184 | + |
| 185 | + // selecting an 11th item should be blocked |
158 | 186 | await unselectedItem.first().click(); |
159 | | - } |
| 187 | + await expect( |
| 188 | + page.getByText("Cannot select more items", { exact: true }), |
| 189 | + ).toBeVisible(); |
160 | 190 |
|
161 | | - await expect(page.getByText("10 of 10 items selected")).toBeVisible(); |
162 | | - |
163 | | - // selecting an 11th item should be blocked |
164 | | - await unselectedItem.first().click(); |
165 | | - await expect( |
166 | | - page.getByText("Cannot select more items", { exact: true }), |
167 | | - ).toBeVisible(); |
168 | | - |
169 | | - await page.getByText("Smoothie").click(); |
| 191 | + await page.getByText("Smoothie").click(); |
| 192 | + } finally { |
| 193 | + await deleteIfExists(baseURL, slug); |
| 194 | + } |
170 | 195 | }); |
171 | 196 |
|
172 | 197 | test("should show warning for inactive number", async ({ page, context }) => { |
|
0 commit comments