88 webkit ,
99 Browser ,
1010} from "@playwright/test" ;
11- import { baseUrlSetup } from "@bciers/e2e/utils/constants" ;
11+ import { baseUrlSetup , GRID_ROOT } from "@bciers/e2e/utils/constants" ;
1212import { DataTestID , MessageTextResponse } from "@bciers/e2e/utils/enums" ;
1313import AxeBuilder from "@axe-core/playwright" ;
1414import path from "node:path" ;
@@ -33,14 +33,21 @@ export async function analyzeAccessibility(
3333 expect ( accessibilityScanResults . violations ) . toEqual ( [ ] ) ;
3434}
3535
36+ /**
37+ * Clicks a button by accessible name and optionally waits for navigation.
38+ * @param page - Playwright Page instance
39+ * @param buttonText - Button accessible name or RegExp
40+ * @param opts.inForm - Scope the button search to a <form> (default: false)
41+ * @param opts.waitForUrl - RegExp to wait for after clicking (optional)
42+ */
3643export async function clickButton (
3744 page : Page ,
3845 buttonText : string | RegExp ,
3946 opts ?: {
4047 inForm ?: boolean ; // default false
4148 waitForUrl ?: RegExp ;
4249 } ,
43- ) {
50+ ) : Promise < void > {
4451 const { inForm = false , waitForUrl } = opts ?? { } ;
4552
4653 const name =
@@ -49,8 +56,18 @@ export async function clickButton(
4956 const root = inForm ? page . locator ( "form" ) : page ;
5057 const button = root . getByRole ( "button" , { name } ) ;
5158
59+ await expect ( button ) . toBeVisible ( { timeout : 30_000 } ) ;
60+ await expect ( button ) . toBeEnabled ( { timeout : 30_000 } ) ;
61+
5262 if ( waitForUrl ) {
53- await Promise . all ( [ page . waitForURL ( waitForUrl ) , button . click ( ) ] ) ;
63+ // Use a lighter wait for SPA navigation
64+ await Promise . all ( [
65+ page . waitForURL ( ( u ) => waitForUrl . test ( u . toString ( ) ) , {
66+ timeout : 30_000 ,
67+ waitUntil : "domcontentloaded" ,
68+ } ) ,
69+ button . click ( ) ,
70+ ] ) ;
5471 } else {
5572 await button . click ( ) ;
5673 }
@@ -83,6 +100,55 @@ export async function fillDropdownByLabel(
83100 await input . fill ( value ) ;
84101}
85102
103+ export async function fillInputValueByLabel (
104+ page : Page ,
105+ label : string | RegExp ,
106+ value : string | number ,
107+ opts ?: {
108+ blur ?: "tab" | "enter" | "none" ; // default "tab"
109+ } ,
110+ ) : Promise < void > {
111+ const { blur = "tab" } = opts ?? { } ;
112+
113+ const name = label instanceof RegExp ? label : new RegExp ( label , "i" ) ;
114+ const field = page . getByLabel ( name ) ;
115+
116+ await expect ( field ) . toBeVisible ( { timeout : 30_000 } ) ;
117+ await expect ( field ) . toBeEnabled ( { timeout : 30_000 } ) ;
118+
119+ await field . click ( ) ;
120+ await field . press ( "Control+A" ) ;
121+ await field . press ( "Backspace" ) ;
122+
123+ await field . fill ( String ( value ) ) ;
124+
125+ if ( blur === "tab" ) await field . press ( "Tab" ) ;
126+ if ( blur === "enter" ) await field . press ( "Enter" ) ;
127+ }
128+
129+ export async function fillInputValueByLocator (
130+ field : Locator ,
131+ value : string | number ,
132+ opts ?: {
133+ blur ?: "tab" | "enter" | "none" ; // default "tab"
134+ } ,
135+ ) : Promise < void > {
136+ const { blur = "tab" } = opts ?? { } ;
137+
138+ await expect ( field ) . toBeVisible ( { timeout : 30_000 } ) ;
139+ await expect ( field ) . toBeEnabled ( { timeout : 30_000 } ) ;
140+
141+ // focus and clear field
142+ await field . click ( ) ;
143+ await field . press ( "Control+A" ) ;
144+ await field . press ( "Backspace" ) ;
145+
146+ await field . fill ( String ( value ) ) ;
147+
148+ if ( blur === "tab" ) await field . press ( "Tab" ) ;
149+ if ( blur === "enter" ) await field . press ( "Enter" ) ;
150+ }
151+
86152export async function checkAllRadioButtons ( page : Page ) {
87153 const radioButtons = page . getByRole ( "radio" , { name : "Yes" } ) ;
88154 // Wait for at least one radio button to be visible before counting.
@@ -100,6 +166,24 @@ export async function checkAllRadioButtons(page: Page) {
100166 }
101167}
102168
169+ export async function checkCheckboxByLabel (
170+ page : Page ,
171+ label : string | RegExp ,
172+ ) : Promise < void > {
173+ const name = label instanceof RegExp ? label : new RegExp ( label , "i" ) ;
174+
175+ const checkbox = page . getByRole ( "checkbox" , { name } ) ;
176+
177+ await expect ( checkbox ) . toBeVisible ( { timeout : 30_000 } ) ;
178+ await expect ( checkbox ) . toBeEnabled ( { timeout : 30_000 } ) ;
179+
180+ if ( ! ( await checkbox . isChecked ( ) ) ) {
181+ await checkbox . check ( ) ;
182+ }
183+
184+ await expect ( checkbox ) . toBeChecked ( ) ;
185+ }
186+
103187// 🛠️ Function: checks expected alert message
104188export async function checkAlertMessage (
105189 page : Page ,
@@ -231,7 +315,7 @@ export async function tableColumnNamesAreCorrect(
231315 expectedColumnNames : string [ ] ,
232316) {
233317 const columnHeaders = page . locator ( ".MuiDataGrid-columnHeaderTitle" ) ;
234- const actualColumnNames = await columnHeaders . allTextContents ( ) ;
318+ const actualColumnNames = await columnHeaders . getByText ( ) ;
235319 expect ( actualColumnNames ) . toEqual ( expectedColumnNames ) ;
236320}
237321
@@ -525,3 +609,60 @@ export function getCrvIdFromUrl({ url }: { url: string }): number {
525609 if ( ! match ) throw new Error ( `Could not extract crvId from URL: ${ url } ` ) ;
526610 return Number ( match [ 1 ] ) ;
527611}
612+
613+ /**
614+ * Wait until the grid is actually "ready":
615+ * - GRID_ROOT exists
616+ * - root + role=grid visible
617+ * - (optional) progressbar/spinner is gone
618+ * - at least one gridcell exists
619+ *
620+ * Tolerates re-mounts (e.g. HMR) with re-check of counts on every attempt
621+ */
622+ export async function waitForGridReady (
623+ page : Page ,
624+ options ?: { timeout ?: number } ,
625+ ) : Promise < void > {
626+ const timeout = options ?. timeout ?? 30_000 ;
627+
628+ await expect ( async ( ) => {
629+ const rootCount = await page . locator ( GRID_ROOT ) . count ( ) ;
630+ expect ( rootCount ) . toBeGreaterThan ( 0 ) ;
631+
632+ const grid = page . locator ( GRID_ROOT ) ;
633+ await expect ( grid ) . toBeVisible ( ) ;
634+
635+ const progressbar = grid . locator ( '[role="progressbar"]' ) ;
636+ const anyCell = grid . locator ( '[role="gridcell"]' ) . first ( ) ;
637+
638+ if ( ( await progressbar . count ( ) ) > 0 ) {
639+ await expect ( progressbar ) . toBeHidden ( ) ;
640+ }
641+
642+ await expect ( anyCell ) . toBeVisible ( ) ;
643+ } ) . toPass ( { timeout } ) ;
644+ }
645+
646+ export async function getGridRowByText (
647+ page : Page ,
648+ rowText : string | RegExp ,
649+ ) : Promise < Locator > {
650+ await waitForGridReady ( page , { timeout : 30_000 } ) ;
651+
652+ const grid = page . locator ( GRID_ROOT ) . first ( ) . locator ( '[role="grid"]' ) . first ( ) ;
653+ const row = grid . getByRole ( "row" ) . filter ( { hasText : rowText } ) . first ( ) ;
654+
655+ await expect ( async ( ) => {
656+ const matchingRowCount = await row . count ( ) ;
657+ const totalRowCount = await grid . getByRole ( "row" ) . count ( ) ;
658+
659+ expect ( totalRowCount ) . toBeGreaterThan ( 0 ) ;
660+ expect ( matchingRowCount ) . toBeGreaterThan ( 0 ) ;
661+ await expect ( row ) . toBeVisible ( ) ;
662+
663+ const cellCount = await row . locator ( '[role="gridcell"]' ) . count ( ) ;
664+ expect ( cellCount ) . toBeGreaterThan ( 0 ) ;
665+ } ) . toPass ( { timeout : 30_000 } ) ;
666+
667+ return row ;
668+ }
0 commit comments