@@ -11,6 +11,13 @@ import {
1111} from 'e2e/data/filters' ;
1212import { 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+
1421export 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 ( ) {
0 commit comments