@@ -25,18 +25,25 @@ const APPLICANT = {
2525}
2626
2727test . describe ( 'Candidate Application Flow' , ( ) => {
28- test ( 'candidate can apply to a published job' , async ( { authenticatedPage, testAccount, context } ) => {
28+ test ( 'candidate can apply to a published job' , async ( { authenticatedPage, testAccount, browser } ) => {
2929 const page = authenticatedPage
3030
3131 // ── Setup: Create and publish a job ──────────────────
3232
3333 await page . goto ( '/dashboard/jobs/new' )
34+ await page . waitForLoadState ( 'networkidle' )
35+ // Wait for the form to be fully hydrated before interacting
36+ await page . getByLabel ( 'Job title' ) . waitFor ( { state : 'visible' , timeout : 15_000 } )
3437 await page . getByLabel ( 'Job title' ) . fill ( JOB_TITLE )
3538 await page . locator ( 'textarea' ) . first ( ) . fill ( JOB_DESCRIPTION )
3639 await page . getByLabel ( 'Location' ) . fill ( JOB_LOCATION )
3740
3841 // Step through wizard (scope to form to avoid header duplicate button)
42+ await page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) . waitFor ( { state : 'attached' , timeout : 10_000 } )
43+ await expect ( page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) ) . toBeEnabled ( { timeout : 10_000 } )
3944 await page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) . click ( )
45+ await page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) . waitFor ( { state : 'attached' , timeout : 10_000 } )
46+ await expect ( page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) ) . toBeEnabled ( { timeout : 10_000 } )
4047 await page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Save & continue' } ) . click ( )
4148 await page . locator ( 'form' ) . getByRole ( 'button' , { name : 'Create job' } ) . click ( )
4249
@@ -58,41 +65,55 @@ test.describe('Candidate Application Flow', () => {
5865
5966 const jobSlug = jobData . slug
6067
61- // ── Candidate flow: Apply in a separate page ─────────
62- // Use a fresh page (no auth cookies) to simulate an anonymous candidate
63- const candidatePage = await context . newPage ( )
64-
65- // Clear cookies for the candidate page so they have no auth session
66- await candidatePage . context ( ) . clearCookies ( )
68+ // ── Candidate flow: Apply in a separate browser context ─
69+ // Use a fresh context (no auth cookies) to simulate an anonymous candidate
70+ const candidateContext = await browser . newContext ( )
71+ const candidatePage = await candidateContext . newPage ( )
6772
6873 await candidatePage . goto ( `/jobs/${ jobSlug } ` )
74+ await candidatePage . waitForLoadState ( 'networkidle' )
6975 await expect ( candidatePage . getByRole ( 'heading' , { name : JOB_TITLE } ) ) . toBeVisible ( )
7076
7177 // Click Apply (use .first() because the page has both "Apply Now" and "Apply for this position" links)
7278 await candidatePage . getByRole ( 'link' , { name : / a p p l y / i } ) . first ( ) . click ( )
7379 await candidatePage . waitForURL ( `**/jobs/${ jobSlug } /apply` , { waitUntil : 'commit' } )
80+ await candidatePage . waitForLoadState ( 'networkidle' )
81+
82+ // Wait for the application form to be fully rendered and hydrated
83+ await candidatePage . getByRole ( 'button' , { name : / s u b m i t / i } ) . waitFor ( { state : 'visible' , timeout : 15_000 } )
7484
7585 // ── Fill in application form ─────────────────────────
7686 await candidatePage . getByLabel ( 'First name' ) . fill ( APPLICANT . firstName )
7787 await candidatePage . getByLabel ( 'Last name' ) . fill ( APPLICANT . lastName )
7888 await candidatePage . getByLabel ( 'Email' ) . fill ( APPLICANT . email )
7989 await candidatePage . getByLabel ( 'Phone' ) . fill ( APPLICANT . phone )
8090
81- // Submit application
82- await candidatePage . getByRole ( 'button' , { name : / s u b m i t / i } ) . click ( )
91+ // Submit application and wait for the API response
92+ const [ applyResponse ] = await Promise . all ( [
93+ candidatePage . waitForResponse (
94+ resp => resp . url ( ) . includes ( `/api/public/jobs/${ jobSlug } /apply` ) && resp . request ( ) . method ( ) === 'POST' ,
95+ { timeout : 30_000 } ,
96+ ) ,
97+ candidatePage . getByRole ( 'button' , { name : / s u b m i t / i } ) . click ( ) ,
98+ ] )
99+
100+ // Verify the API responded successfully (200 or 201)
101+ const applyStatus = applyResponse . status ( )
102+ expect ( applyStatus , `Apply API returned ${ applyStatus } ` ) . toBeGreaterThanOrEqual ( 200 )
103+ expect ( applyStatus , `Apply API returned ${ applyStatus } ` ) . toBeLessThan ( 300 )
83104
84105 // ── Verify confirmation page ─────────────────────────
85- await candidatePage . waitForURL ( `**/jobs/${ jobSlug } /confirmation` , { waitUntil : 'commit' } )
86- await expect ( candidatePage . getByText ( ' Application Submitted' ) ) . toBeVisible ( )
106+ await candidatePage . waitForURL ( `**/jobs/${ jobSlug } /confirmation` , { waitUntil : 'commit' , timeout : 15_000 } )
107+ await expect ( candidatePage . getByRole ( 'heading' , { name : ' Application Submitted!' } ) ) . toBeVisible ( )
87108 await expect ( candidatePage . getByText ( JOB_TITLE ) ) . toBeVisible ( )
88109
89110 await candidatePage . close ( )
111+ await candidateContext . close ( )
90112
91113 // ── Verify application appears in recruiter dashboard ─
92114 // Navigate to the job's candidates page
93115 await page . goto ( `/dashboard/jobs/${ jobId } /candidates` )
94- await expect ( page . getByText ( APPLICANT . firstName ) ) . toBeVisible ( { timeout : 10_000 } )
95- await expect ( page . getByText ( APPLICANT . lastName ) ) . toBeVisible ( )
96- await expect ( page . getByText ( APPLICANT . email ) ) . toBeVisible ( )
116+ await expect ( page . getByRole ( 'cell' , { name : `${ APPLICANT . firstName } ${ APPLICANT . lastName } ` } ) ) . toBeVisible ( { timeout : 10_000 } )
117+ await expect ( page . getByRole ( 'cell' , { name : APPLICANT . email } ) ) . toBeVisible ( )
97118 } )
98119} )
0 commit comments