11import { test , type Page } from '@playwright/test' ;
22import { ELEMENT_REGISTRY } from '../../src/lib/elements/registry' ;
3+ import {
4+ deliveryContainer ,
5+ getSessionState ,
6+ interactOnce ,
7+ switchMode ,
8+ switchRole ,
9+ waitForSessionMutation ,
10+ } from './test-helpers' ;
311
412type ViewKind = 'deliver' | 'author' | 'print' ;
13+ type StrategyKind = 'esm' | 'iife' ;
514
615interface SmokeCase {
716 element : string ;
817 view : ViewKind ;
18+ strategy : StrategyKind ;
19+ hasSession : boolean ;
920 url : string ;
1021}
1122
@@ -36,52 +47,84 @@ const IGNORE_CONSOLE_PATTERNS = [
3647 / T h e p s e u d o c l a s s " : n t h - c h i l d " i s p o t e n t i a l l y u n s a f e / i,
3748] ;
3849
50+ const MATRIX_STRATEGIES = ( process . env . MATRIX_STRATEGIES ?. trim ( ) || 'esm' )
51+ . split ( ',' )
52+ . map ( ( value ) => value . trim ( ) )
53+ . filter ( ( value ) : value is StrategyKind => value === 'esm' || value === 'iife' ) ;
54+
3955function buildCases ( ) : SmokeCase [ ] {
4056 const cases : SmokeCase [ ] = [ ] ;
41- for ( const element of ELEMENT_REGISTRY ) {
42- cases . push ( {
43- element : element . name ,
44- view : 'deliver' ,
45- url : `/${ element . name } /deliver?mode=gather&role=student&player=iife` ,
46- } ) ;
47- if ( element . hasAuthor ) {
48- cases . push ( {
49- element : element . name ,
50- view : 'author' ,
51- url : `/${ element . name } /author?demo=default&player=iife` ,
52- } ) ;
53- }
54- if ( element . hasPrint ) {
55- cases . push ( {
56- element : element . name ,
57- view : 'print' ,
58- url : `/${ element . name } /print?role=student&player=iife` ,
59- } ) ;
60- }
61-
62- // Keep an explicit regression case for known number-line IIFE/runtime interactions.
63- if ( element . name === 'number-line' ) {
57+ for ( const strategy of MATRIX_STRATEGIES ) {
58+ for ( const element of ELEMENT_REGISTRY ) {
6459 cases . push ( {
6560 element : element . name ,
6661 view : 'deliver' ,
67- url : `/${ element . name } /deliver?demo=basic-points&mode=gather&role=student&player=iife` ,
62+ strategy,
63+ hasSession : element . hasSession ,
64+ url : `/${ element . name } /deliver?mode=gather&role=student&player=${ strategy } ` ,
6865 } ) ;
66+ if ( element . hasAuthor ) {
67+ cases . push ( {
68+ element : element . name ,
69+ view : 'author' ,
70+ strategy,
71+ hasSession : element . hasSession ,
72+ url : `/${ element . name } /author?demo=default&player=${ strategy } ` ,
73+ } ) ;
74+ }
75+ if ( element . hasPrint ) {
76+ cases . push ( {
77+ element : element . name ,
78+ view : 'print' ,
79+ strategy,
80+ hasSession : element . hasSession ,
81+ url : `/${ element . name } /print?role=student&player=${ strategy } ` ,
82+ } ) ;
83+ }
84+
85+ // Keep an explicit regression case for known number-line IIFE/runtime interactions.
86+ if ( element . name === 'number-line' && strategy === 'iife' ) {
87+ cases . push ( {
88+ element : element . name ,
89+ view : 'deliver' ,
90+ strategy,
91+ hasSession : element . hasSession ,
92+ url : `/${ element . name } /deliver?demo=basic-points&mode=gather&role=student&player=${ strategy } ` ,
93+ } ) ;
94+ }
6995 }
7096 }
7197 return cases ;
7298}
7399
74- async function waitForIifeSettle ( page : Page , view : ViewKind , timeoutMs = 20_000 ) {
75- await page . waitForFunction (
76- ( ) => {
77- const iifeLoading = Array . from ( document . querySelectorAll ( '.loading' ) ) . some ( ( node ) =>
78- / I I F E / i. test ( node . textContent || '' )
79- ) ;
80- return ! iifeLoading ;
81- } ,
82- undefined ,
83- { timeout : timeoutMs }
84- ) ;
100+ async function waitForStrategySettle (
101+ page : Page ,
102+ view : ViewKind ,
103+ strategy : StrategyKind ,
104+ timeoutMs = 20_000
105+ ) {
106+ if ( strategy === 'iife' ) {
107+ await page . waitForFunction (
108+ ( ) => {
109+ const iifeLoading = Array . from ( document . querySelectorAll ( '.loading' ) ) . some ( ( node ) =>
110+ / I I F E / i. test ( node . textContent || '' )
111+ ) ;
112+ return ! iifeLoading ;
113+ } ,
114+ undefined ,
115+ { timeout : timeoutMs }
116+ ) ;
117+ } else {
118+ await page . waitForFunction (
119+ ( ) => {
120+ const loading = document . querySelector ( '.loading' ) ;
121+ const error = document . querySelector ( '.error' ) ;
122+ return ! loading && ! error ;
123+ } ,
124+ undefined ,
125+ { timeout : timeoutMs }
126+ ) ;
127+ }
85128
86129 // Route-specific UI markers prove the view actually rendered.
87130 if ( view === 'deliver' ) {
@@ -106,16 +149,111 @@ function findCriticalConsole(messages: string[]): string[] {
106149 } ) ;
107150}
108151
109- test . describe ( 'IIFE smoke matrix across PIE elements' , ( ) => {
152+ const EVALUATE_SIGNAL_SELECTOR =
153+ '[data-testid="score-value"], [data-testid="scoring-panel"], [data-testid="show-correct-answer"], button:has-text("Show correct answer"), button:has-text("Hide correct answer")' ;
154+
155+ const REQUIRE_EVALUATE_SIGNAL_ELEMENTS = new Set ( [
156+ 'multiple-choice' ,
157+ 'ebsr' ,
158+ 'matrix' ,
159+ 'match' ,
160+ 'likert' ,
161+ 'inline-dropdown' ,
162+ 'select-text' ,
163+ 'math-inline' ,
164+ 'math-templated' ,
165+ ] ) ;
166+
167+ const REQUIRE_SESSION_MUTATION_ELEMENTS = new Set ( [ 'multiple-choice' , 'ebsr' ] ) ;
168+
169+ async function synthesizeSessionChanged ( page : Page ) : Promise < boolean > {
170+ return await page . evaluate ( ( token ) => {
171+ const host = document . querySelector ( 'pie-element-player' ) as any ;
172+ if ( ! ( host instanceof HTMLElement ) ) {
173+ return false ;
174+ }
175+ const innerElement =
176+ host . querySelector ( '.demo-element-player > *:not(.loading):not(.error)' ) ??
177+ host . querySelector ( '.element-container > *:not(.loading):not(.error)' ) ;
178+ if ( ! ( innerElement instanceof HTMLElement ) ) {
179+ return false ;
180+ }
181+ const currentSession =
182+ host . session && typeof host . session === 'object'
183+ ? JSON . parse ( JSON . stringify ( host . session ) )
184+ : { } ;
185+ const nextSession = { ...currentSession , __matrixMarker : token } ;
186+ innerElement . dispatchEvent (
187+ new CustomEvent ( 'session-changed' , {
188+ detail : { session : nextSession } ,
189+ bubbles : true ,
190+ composed : true ,
191+ } )
192+ ) ;
193+ return true ;
194+ } , `matrix-${ Date . now ( ) } ` ) ;
195+ }
196+
197+ async function verifyDeliveryInteractionAndEvaluate (
198+ page : Page ,
199+ item : SmokeCase
200+ ) : Promise < string | null > {
201+ if ( ! item . hasSession ) {
202+ return null ;
203+ }
204+
205+ const root = deliveryContainer ( page ) ;
206+ try {
207+ await root . waitFor ( { state : 'visible' , timeout : 15_000 } ) ;
208+ } catch ( err : any ) {
209+ return `Delivery root not visible: ${ err ?. message || String ( err ) } ` ;
210+ }
211+
212+ const before = await getSessionState ( page ) ;
213+
214+ try {
215+ await interactOnce ( page , root ) ;
216+ } catch ( err : any ) {
217+ const fallbackDispatched = await synthesizeSessionChanged ( page ) ;
218+ if ( ! fallbackDispatched ) {
219+ return `No interactive control found: ${ err ?. message || String ( err ) } ` ;
220+ }
221+ }
222+
223+ const after = await waitForSessionMutation ( page , before , 10_000 ) ;
224+ if (
225+ REQUIRE_SESSION_MUTATION_ELEMENTS . has ( item . element ) &&
226+ JSON . stringify ( after ?? { } ) === JSON . stringify ( before ?? { } )
227+ ) {
228+ return 'Session did not mutate after delivery interaction' ;
229+ }
230+
231+ try {
232+ await switchRole ( page , 'instructor' ) ;
233+ await switchMode ( page , 'evaluate' ) ;
234+ if ( REQUIRE_EVALUATE_SIGNAL_ELEMENTS . has ( item . element ) ) {
235+ await page
236+ . locator ( EVALUATE_SIGNAL_SELECTOR )
237+ . first ( )
238+ . waitFor ( { state : 'visible' , timeout : 15_000 } ) ;
239+ }
240+ } catch ( err : any ) {
241+ return `Evaluate/correct-answer signal not visible: ${ err ?. message || String ( err ) } ` ;
242+ }
243+
244+ return null ;
245+ }
246+
247+ test . describe ( 'Strategy smoke matrix across PIE elements' , ( ) => {
110248 test ( 'all elements/views render without critical runtime or build failures' , async ( { page } ) => {
111249 test . setTimeout ( 15 * 60 * 1000 ) ;
112250
113251 const failures : SmokeFailure [ ] = [ ] ;
114252 const matrix = buildCases ( ) ;
115253
116254 for ( const item of matrix ) {
117- await test . step ( `${ item . element } :: ${ item . view } ` , async ( ) => {
118- console . log ( `[smoke] checking ${ item . element } :: ${ item . view } ` ) ;
255+ await test . step ( `${ item . element } :: ${ item . view } :: ${ item . strategy } ` , async ( ) => {
256+ console . log ( `[smoke] checking ${ item . element } :: ${ item . view } :: ${ item . strategy } ` ) ;
119257 const consoleMessages : string [ ] = [ ] ;
120258 const pageErrors : string [ ] = [ ] ;
121259
@@ -134,7 +272,7 @@ test.describe('IIFE smoke matrix across PIE elements', () => {
134272
135273 try {
136274 await page . goto ( item . url , { waitUntil : 'domcontentloaded' } ) ;
137- await waitForIifeSettle ( page , item . view ) ;
275+ await waitForStrategySettle ( page , item . view , item . strategy ) ;
138276
139277 const errorNodes = page . locator ( '.error' ) ;
140278 const errorCount = await errorNodes . count ( ) ;
@@ -170,6 +308,19 @@ test.describe('IIFE smoke matrix across PIE elements', () => {
170308 reason : 'Critical console/runtime errors' ,
171309 details : criticalConsole . slice ( 0 , 6 ) . join ( '\n' ) ,
172310 } ) ;
311+ return ;
312+ }
313+
314+ if ( item . view === 'deliver' ) {
315+ const verifyError = await verifyDeliveryInteractionAndEvaluate ( page , item ) ;
316+ if ( verifyError ) {
317+ failures . push ( {
318+ element : item . element ,
319+ view : item . view ,
320+ url : item . url ,
321+ reason : verifyError ,
322+ } ) ;
323+ }
173324 }
174325 } catch ( err : any ) {
175326 failures . push ( {
0 commit comments