11import { test , expect } from '@playwright/test'
22import { authStatePath , navigateToDetail , selectOption } from './helpers'
3+ import {
4+ snapshotMailbox ,
5+ waitForNewMessages ,
6+ getBody ,
7+ getToAddresses ,
8+ assertSentFromApp ,
9+ findBySubject ,
10+ } from './mailpit'
311
412const APPLICATION_REJECT_ID = '00000000-0000-4000-c000-000000000004' // student4 on topic 1, NOT_ASSESSED
513const APPLICATION_ACCEPT_ID = '00000000-0000-4000-c000-000000000005' // student5 on topic 2, NOT_ASSESSED
@@ -14,7 +22,6 @@ test.describe('Application Review Workflow', () => {
1422 if ( ! loaded ) return // Application not accessible (may have been modified by a parallel test)
1523
1624 // Check if application still has the review form (NOT_ASSESSED state)
17- // A prior test run may have rejected this application and DB wasn't re-seeded
1825 const thesisTitle = page . getByLabel ( 'Thesis Title' )
1926 const hasReviewForm = await thesisTitle . isVisible ( { timeout : 5_000 } ) . catch ( ( ) => false )
2027 if ( ! hasReviewForm ) {
@@ -28,24 +35,43 @@ test.describe('Application Review Workflow', () => {
2835
2936 // Modal should open with "Reject Application" title
3037 await expect ( page . getByRole ( 'dialog' ) ) . toBeVisible ( { timeout : 5_000 } )
31- await expect (
32- page . getByRole ( 'dialog' ) . getByText ( 'Reject Application' ) . first ( ) ,
33- ) . toBeVisible ( )
38+ await expect ( page . getByRole ( 'dialog' ) . getByText ( 'Reject Application' ) . first ( ) ) . toBeVisible ( )
3439
35- // "Topic requirements not met" should be the default selected reason for topic-based applications
40+ // "Topic requirements not met" should be the default selected reason
3641 await expect ( page . getByText ( 'Topic requirements not met' ) ) . toBeVisible ( )
3742
3843 // "Notify Student" checkbox should be checked by default
3944 const notifyCheckbox = page . getByRole ( 'dialog' ) . getByLabel ( 'Notify Student' )
4045 await expect ( notifyCheckbox ) . toBeChecked ( )
4146
47+ // Snapshot mailbox before the action
48+ const beforeIds = await snapshotMailbox ( 'student4@test.local' )
49+
4250 // Click "Reject Application" button in the modal
4351 await page . getByRole ( 'dialog' ) . getByRole ( 'button' , { name : 'Reject Application' } ) . click ( )
4452
4553 // Verify success notification
4654 await expect ( page . getByText ( 'Application rejected successfully' ) ) . toBeVisible ( {
4755 timeout : 10_000 ,
4856 } )
57+
58+ // --- Email verification ---
59+ // Rejection with "Notify Student" sends APPLICATION_REJECTED_TOPIC_REQUIREMENTS
60+ const newEmails = await waitForNewMessages ( 'student4@test.local' , beforeIds )
61+ expect ( newEmails . length ) . toBeGreaterThanOrEqual ( 1 )
62+
63+ const rejectionEmail = findBySubject ( newEmails , 'Thesis Application Rejection' )
64+ expect ( rejectionEmail , 'Rejection email with correct subject should be sent' ) . toBeDefined ( )
65+ assertSentFromApp ( rejectionEmail ! )
66+ expect ( getToAddresses ( rejectionEmail ! ) ) . toContain ( 'student4@test.local' )
67+
68+ // Body should greet the student by first name and reference the topic title
69+ const body = getBody ( rejectionEmail ! )
70+ expect ( body , 'Rejection email should greet the student' ) . toContain ( 'Student4' )
71+ expect ( body , 'Rejection email should reference the topic title' ) . toContain (
72+ 'Automated Code Review Using Large Language Models' ,
73+ )
74+ expect ( body , 'Rejection email should mention requirements' ) . toContain ( 'requirements' )
4975 } )
5076
5177 test ( 'advisor can accept a NOT_ASSESSED application' , async ( { page } ) => {
@@ -54,7 +80,6 @@ test.describe('Application Review Workflow', () => {
5480 if ( ! loaded ) return // Application not accessible
5581
5682 // Check if application still has the review form (NOT_ASSESSED state)
57- // A prior test run may have accepted this application and DB wasn't re-seeded
5883 const thesisTitle = page . getByLabel ( 'Thesis Title' )
5984 const hasReviewForm = await thesisTitle . isVisible ( { timeout : 5_000 } ) . catch ( ( ) => false )
6085 if ( ! hasReviewForm ) {
@@ -63,8 +88,6 @@ test.describe('Application Review Workflow', () => {
6388
6489 // Verify the acceptance form has pre-filled fields from the topic
6590 await expect ( thesisTitle ) . not . toHaveValue ( '' )
66-
67- // Thesis Type should be pre-filled
6891 await expect ( page . getByRole ( 'textbox' , { name : 'Thesis Type' } ) ) . toBeVisible ( )
6992
7093 // Thesis Language may not be pre-filled — fill it if empty
@@ -89,6 +112,9 @@ test.describe('Application Review Workflow', () => {
89112 timeout : 10_000 ,
90113 } )
91114
115+ // Snapshot mailbox for student5 before the action
116+ const beforeIds = await snapshotMailbox ( 'student5@test.local' )
117+
92118 // Click "Accept" button
93119 const acceptButton = page . getByRole ( 'button' , { name : 'Accept' , exact : true } )
94120 await expect ( acceptButton ) . toBeEnabled ( { timeout : 10_000 } )
@@ -98,5 +124,34 @@ test.describe('Application Review Workflow', () => {
98124 await expect ( page . getByText ( 'Application accepted successfully' ) ) . toBeVisible ( {
99125 timeout : 30_000 ,
100126 } )
127+
128+ // --- Email verification ---
129+ // Acceptance sends APPLICATION_ACCEPTED + THESIS_CREATED both to student5
130+ const newEmails = await waitForNewMessages ( 'student5@test.local' , beforeIds , 2 , 30_000 )
131+
132+ // Verify acceptance email was sent to student5 (APPLICATION_ACCEPTED template)
133+ const acceptanceEmail = findBySubject ( newEmails , 'Thesis Application Acceptance' )
134+ expect ( acceptanceEmail , 'Acceptance email should be sent' ) . toBeDefined ( )
135+ assertSentFromApp ( acceptanceEmail ! )
136+ expect ( getToAddresses ( acceptanceEmail ! ) ) . toContain ( 'student5@test.local' )
137+
138+ // Body should greet student, mention the advisor, and include a thesis link
139+ const acceptBody = getBody ( acceptanceEmail ! )
140+ expect ( acceptBody , 'Should greet the student by first name' ) . toContain ( 'Student5' )
141+ expect ( acceptBody , 'Should mention the advisor for coordination' ) . toContain ( 'advisor' )
142+ expect ( acceptBody , 'Should contain a link to the thesis' ) . toContain ( '/theses/' )
143+
144+ // Verify thesis creation email was also sent to student5 (THESIS_CREATED template)
145+ const thesisEmail = findBySubject ( newEmails , 'Thesis Created' )
146+ expect ( thesisEmail , 'Thesis creation email should be sent' ) . toBeDefined ( )
147+ assertSentFromApp ( thesisEmail ! )
148+ expect ( getToAddresses ( thesisEmail ! ) ) . toContain ( 'student5@test.local' )
149+
150+ // Body should contain the thesis title and a link to the thesis
151+ const thesisBody = getBody ( thesisEmail ! )
152+ expect ( thesisBody , 'Should contain the thesis title' ) . toContain (
153+ 'Continuous Integration Pipeline Optimization' ,
154+ )
155+ expect ( thesisBody , 'Should contain a link to the thesis' ) . toContain ( '/theses/' )
101156 } )
102157} )
0 commit comments