Skip to content

Commit 0a82fed

Browse files
authored
chore: Update public e2e inefficiencies (#1760)
* chore: Update public e2e inefficiencies * chore: Increase district render timeout
1 parent dd23385 commit 0a82fed

6 files changed

Lines changed: 95 additions & 128 deletions

File tree

public/frontend/e2e/poms/components/filter.ts

Lines changed: 65 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import {
1111
} from 'e2e/data/filters';
1212
import { FilterEnum, FilterGroup } from 'e2e/enum/filter';
1313

14+
// The District group is populated from an API call and is the slowest, most
15+
// latency-sensitive part of the menu on deployed environments. Give its initial
16+
// render a generous window (matching the 30s default of the `locator.waitFor`
17+
// this readiness check used to rely on) rather than the 10s `expect` timeout, so
18+
// a slow-but-healthy district fetch doesn't flake the test.
19+
const DISTRICT_RENDER_TIMEOUT_MS = 30_000;
20+
1421
export class FilterPOM {
1522
readonly page: Page;
1623

@@ -84,54 +91,48 @@ export class FilterPOM {
8491
await showAllButton.click();
8592
}
8693

87-
async toggleFilterOn(filterGroup: Locator, filterPrefix: string) {
88-
await filterGroup.waitFor({ state: 'visible' });
89-
await filterGroup.locator('.form-check').first().waitFor({
90-
state: 'visible',
91-
timeout: 10000,
92-
});
94+
/**
95+
* Resolve the checkbox input for a filter option by its label prefix.
96+
*
97+
* Single source of truth for the "label -> `for` -> input" lookup used by every
98+
* toggle/assert helper below, so the locator strategy stays consistent and the
99+
* intermediate waits live in one place.
100+
*/
101+
private async getCheckboxByLabel(
102+
filterGroup: Locator,
103+
filterPrefix: string,
104+
): Promise<Locator> {
93105
const label = filterGroup
94106
.locator('label')
95107
.filter({ hasText: new RegExp(`^${filterPrefix}`) });
96108
await label.waitFor({ state: 'visible', timeout: 10000 });
97109
const labelFor = await label.getAttribute('for');
98110
const checkbox = filterGroup.locator(`input[id="${labelFor}"]`);
99111
await checkbox.waitFor({ state: 'visible', timeout: 5000 });
100-
expect(checkbox).not.toBeChecked();
112+
return checkbox;
113+
}
114+
115+
async toggleFilterOn(filterGroup: Locator, filterPrefix: string) {
116+
const checkbox = await this.getCheckboxByLabel(filterGroup, filterPrefix);
117+
await expect(checkbox).not.toBeChecked();
101118
await checkbox.check();
102119
await expect(checkbox).toBeChecked();
103120
}
104121

105122
async toggleFilterOff(filterGroup: Locator, filterPrefix: string) {
106-
const label = filterGroup
107-
.locator('label')
108-
.filter({ hasText: new RegExp(`^${filterPrefix}`) });
109-
await label.waitFor({ state: 'visible', timeout: 5000 });
110-
const labelFor = await label.getAttribute('for');
111-
const checkbox = filterGroup.locator(`input[id="${labelFor}"]`);
112-
await checkbox.waitFor({ state: 'visible', timeout: 5000 });
113-
expect(checkbox).toBeChecked();
123+
const checkbox = await this.getCheckboxByLabel(filterGroup, filterPrefix);
124+
await expect(checkbox).toBeChecked();
114125
await checkbox.uncheck();
115126
await expect(checkbox).not.toBeChecked();
116127
}
117128

118129
async checkIsFilterToggledOn(filterGroup: Locator, filterPrefix: string) {
119-
const label = filterGroup
120-
.locator('label')
121-
.filter({ hasText: new RegExp(`^${filterPrefix}`) });
122-
await label.waitFor({ state: 'visible', timeout: 5000 });
123-
const labelFor = await label.getAttribute('for');
124-
const checkbox = filterGroup.locator(`input[id="${labelFor}"]`);
125-
126-
await checkbox.waitFor({ state: 'visible', timeout: 5000 });
130+
const checkbox = await this.getCheckboxByLabel(filterGroup, filterPrefix);
127131
await expect(checkbox).toBeChecked();
128132
}
129133

130134
async checkIsFilterToggledOff(filterGroup: Locator, filterPrefix: string) {
131-
const checkbox = filterGroup.getByRole('checkbox', {
132-
name: new RegExp(`^${filterPrefix}`),
133-
});
134-
await checkbox.waitFor({ state: 'visible' });
135+
const checkbox = await this.getCheckboxByLabel(filterGroup, filterPrefix);
135136
await expect(checkbox).not.toBeChecked();
136137
}
137138

@@ -144,25 +145,33 @@ export class FilterPOM {
144145
await this.clickShowAllFilters(filterGroup);
145146
}
146147

147-
for (const filter of filterOptions) {
148-
const { label } = filter;
149-
150-
await filterGroup
151-
.locator('label', { hasText: label })
152-
.first()
153-
.waitFor({ state: 'visible' });
154-
}
148+
// Verify each expected option renders (same coverage as before), but resolve
149+
// the auto-retrying checks concurrently instead of as an O(n) sequence of
150+
// per-option `waitFor`s. Note: the data files aren't necessarily exhaustive
151+
// (e.g. the Type group renders more options than are listed), so this asserts
152+
// "these options are present", not an exact count.
153+
await Promise.all(
154+
filterOptions.map(({ label }) =>
155+
expect(
156+
filterGroup.locator('label', { hasText: label }).first(),
157+
).toBeVisible(),
158+
),
159+
);
155160

156161
if (isShowMore) {
157162
await this.clickShowLessFilters(filterGroup);
158163
}
159164
}
160165

161166
async verifyDistrictFilterGroup() {
162-
const options = this.districtFilters.locator('.form-check label');
163-
await options.first().waitFor({ state: 'visible' });
164-
const count = await options.count();
165-
expect(count).toBeGreaterThan(0);
167+
// Districts are data-driven, so assert "at least one option" with an
168+
// auto-retrying matcher rather than a one-shot count() snapshot.
169+
await expect(
170+
this.districtFilters.locator('.form-check label').first(),
171+
).toBeVisible({ timeout: DISTRICT_RENDER_TIMEOUT_MS });
172+
await expect(
173+
this.districtFilters.locator('.form-check label'),
174+
).not.toHaveCount(0);
166175
}
167176

168177
async verifyTypeFilterGroup() {
@@ -203,6 +212,13 @@ export class FilterPOM {
203212
await this.verifyFilterGroup(this.feesFilters, feesFilterOptions);
204213
}
205214

215+
/**
216+
* Exhaustively verify every filter group renders with its expected options.
217+
*
218+
* This is the full rendering assertion and is intentionally heavy — it belongs
219+
* in the dedicated "filter menu renders" test only. Functional tests that merely
220+
* need the panel to be interactive should use {@link waitForFilterMenuReady}.
221+
*/
206222
async verifyInitialFilterMenu() {
207223
await this.verifyDistrictFilterGroup();
208224
await this.verifyTypeFilterGroup();
@@ -213,52 +229,16 @@ export class FilterPOM {
213229
await this.verifyAccessTypeFilterGroup();
214230
}
215231

216-
async verifyFilterResultsListener({
217-
type,
218-
activities,
219-
}: {
220-
type?: string[];
221-
activities?: string[];
222-
}) {
223-
this.page.on('response', async (response) => {
224-
// We can't use this to check district, facilities, or access type
225-
// because the API doesn't return data for those filters
226-
const url = response.url();
227-
const status = response.status();
228-
229-
// Only parse successful JSON responses
230-
if (
231-
status !== 200 ||
232-
!response.headers()['content-type']?.includes('application/json')
233-
) {
234-
return;
235-
}
236-
237-
if (url.includes('type=') && type) {
238-
const json = await response.json();
239-
const results = json.data.map((item: any) => item.rec_resource_type);
240-
expect(results).toEqual(expect.arrayContaining(type));
241-
}
242-
243-
if (url.includes('activities=') && activities) {
244-
const json = await response.json();
245-
const results = json.data.map((item: any) => item.recreation_activity);
246-
results.forEach((activities: any) => {
247-
const relevantActivities = activities.filter((activity: any) =>
248-
activities.every((element: any) =>
249-
activity.description.includes(element),
250-
),
251-
);
252-
relevantActivities.forEach((activity: any) => {
253-
activities.forEach((option: any) => {
254-
expect(activity.description).toEqual(
255-
expect.stringContaining(option),
256-
);
257-
});
258-
});
259-
});
260-
}
261-
});
232+
/**
233+
* Lightweight readiness gate for functional tests: waits only until the filter
234+
* panel is interactive (first group's first option visible). Replaces the heavy
235+
* {@link verifyInitialFilterMenu} prelude that most tests don't need — a single
236+
* environment blip in that prelude used to fail dozens of unrelated tests.
237+
*/
238+
async waitForFilterMenuReady() {
239+
await expect(
240+
this.districtFilters.locator('.form-check label').first(),
241+
).toBeVisible({ timeout: DISTRICT_RENDER_TIMEOUT_MS });
262242
}
263243

264244
async openMobileFilterMenu() {

public/frontend/e2e/workflows/search/filter.spec.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ test.describe('Search page filter menu workflows', () => {
1515

1616
await searchPage.route();
1717

18-
await filter.verifyInitialFilterMenu();
18+
await filter.waitForFilterMenuReady();
1919

2020
await filter.toggleFilterOn(filter.districtFilters, 'Chilliwack');
2121

@@ -33,7 +33,7 @@ test.describe('Search page filter menu workflows', () => {
3333

3434
await searchPage.route();
3535

36-
await filter.verifyInitialFilterMenu();
36+
await filter.waitForFilterMenuReady();
3737

3838
await filter.toggleFilterOn(filter.districtFilters, 'Chilliwack');
3939

@@ -55,9 +55,7 @@ test.describe('Search page filter menu workflows', () => {
5555

5656
await searchPage.route();
5757

58-
await filter.verifyInitialFilterMenu();
59-
60-
await filter.verifyFilterResultsListener({ type: ['Recreation trail'] });
58+
await filter.waitForFilterMenuReady();
6159

6260
await filter.toggleFilterOn(filter.typeFilters, RecResourceType.TRAIL);
6361

@@ -75,18 +73,12 @@ test.describe('Search page filter menu workflows', () => {
7573

7674
await searchPage.route();
7775

78-
await filter.verifyInitialFilterMenu();
79-
80-
await filter.verifyFilterResultsListener({ type: ['Recreation trail'] });
76+
await filter.waitForFilterMenuReady();
8177

8278
await filter.toggleFilterOn(filter.typeFilters, RecResourceType.TRAIL);
8379

8480
await searchPage.waitForResults();
8581

86-
await filter.verifyFilterResultsListener({
87-
type: ['Recreation trail', 'Recreation site'],
88-
});
89-
9082
await filter.toggleFilterOn(filter.typeFilters, RecResourceType.SITE);
9183

9284
await utils.checkExpectedUrlParams('type=RTE_SIT');
@@ -101,9 +93,7 @@ test.describe('Search page filter menu workflows', () => {
10193

10294
await searchPage.route();
10395

104-
await filter.verifyInitialFilterMenu();
105-
106-
await filter.verifyFilterResultsListener({ activities: ['Camping'] });
96+
await filter.waitForFilterMenuReady();
10797

10898
await filter.toggleFilterOn(filter.thingsToDoFilters, 'Camping');
10999

@@ -121,18 +111,14 @@ test.describe('Search page filter menu workflows', () => {
121111

122112
await searchPage.route();
123113

124-
await filter.verifyInitialFilterMenu();
114+
await filter.waitForFilterMenuReady();
125115

126116
await filter.toggleFilterOn(filter.thingsToDoFilters, 'Angling');
127117

128118
await filter.toggleFilterOn(filter.thingsToDoFilters, 'Camping');
129119

130120
await filter.clickShowAllFilters(filter.thingsToDoFilters);
131121

132-
await filter.verifyFilterResultsListener({
133-
activities: ['Angling', 'Camping', 'Canoeing'],
134-
});
135-
136122
await filter.toggleFilterOn(filter.thingsToDoFilters, 'Canoeing');
137123

138124
await utils.checkExpectedUrlParams('activities=1_32_3');
@@ -147,7 +133,7 @@ test.describe('Search page filter menu workflows', () => {
147133

148134
await searchPage.route();
149135

150-
await filter.verifyInitialFilterMenu();
136+
await filter.waitForFilterMenuReady();
151137

152138
await filter.toggleFilterOn(filter.facilitiesFilters, 'Toilets');
153139

@@ -165,7 +151,7 @@ test.describe('Search page filter menu workflows', () => {
165151

166152
await searchPage.route();
167153

168-
await filter.verifyInitialFilterMenu();
154+
await filter.waitForFilterMenuReady();
169155

170156
await filter.toggleFilterOn(filter.facilitiesFilters, 'Toilets');
171157

@@ -183,7 +169,7 @@ test.describe('Search page filter menu workflows', () => {
183169

184170
await searchPage.route();
185171

186-
await filter.verifyInitialFilterMenu();
172+
await filter.waitForFilterMenuReady();
187173

188174
await filter.toggleFilterOn(filter.accessTypeFilters, 'Road access');
189175

@@ -201,7 +187,7 @@ test.describe('Search page filter menu workflows', () => {
201187

202188
await searchPage.route();
203189

204-
await filter.verifyInitialFilterMenu();
190+
await filter.waitForFilterMenuReady();
205191

206192
await filter.toggleFilterOn(filter.accessTypeFilters, 'Boat-in access');
207193

@@ -229,7 +215,7 @@ test.describe('Search page filter menu workflows', () => {
229215

230216
await searchPage.route();
231217

232-
await filter.verifyInitialFilterMenu();
218+
await filter.waitForFilterMenuReady();
233219

234220
await filter.toggleFilterOn(filter.districtFilters, 'Chilliwack');
235221

@@ -241,10 +227,6 @@ test.describe('Search page filter menu workflows', () => {
241227

242228
await filter.toggleFilterOn(filter.facilitiesFilters, 'Toilets');
243229

244-
await filter.verifyFilterResultsListener({
245-
type: ['Recreation site'],
246-
});
247-
248230
await searchPage.waitForResults();
249231

250232
await utils.checkExpectedUrlParams(
@@ -268,7 +250,7 @@ test.describe('Search page filter menu workflows', () => {
268250

269251
await searchPage.route();
270252

271-
await filter.verifyInitialFilterMenu();
253+
await filter.waitForFilterMenuReady();
272254

273255
await filter.toggleFilterOn(filter.districtFilters, 'Chilliwack');
274256

@@ -292,7 +274,7 @@ test.describe('Search page filter menu workflows', () => {
292274

293275
await searchPage.route();
294276

295-
await filter.verifyInitialFilterMenu();
277+
await filter.waitForFilterMenuReady();
296278

297279
await filter.clickShowAllFilters(filter.districtFilters);
298280

public/frontend/e2e/workflows/search/filterChips.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test.describe('Filter chip workflows', () => {
1818

1919
await searchPage.route();
2020

21-
await filter.verifyInitialFilterMenu();
21+
await filter.waitForFilterMenuReady();
2222

2323
await filter.toggleFilterOn(filter.districtFilters, DISTRICT);
2424

@@ -42,7 +42,7 @@ test.describe('Filter chip workflows', () => {
4242

4343
await searchPage.route();
4444

45-
await filter.verifyInitialFilterMenu();
45+
await filter.waitForFilterMenuReady();
4646

4747
await filter.toggleFilterOn(filter.districtFilters, DISTRICT);
4848

@@ -92,7 +92,7 @@ test.describe('Filter chip workflows', () => {
9292

9393
await searchPage.route();
9494

95-
await filter.verifyInitialFilterMenu();
95+
await filter.waitForFilterMenuReady();
9696

9797
await filter.toggleFilterOn(filter.districtFilters, DISTRICT);
9898

0 commit comments

Comments
 (0)