@@ -33,6 +33,10 @@ async function waitForHostSettled(page: Page) {
3333 ) ;
3434}
3535
36+ function hasBothStrategies ( ) : boolean {
37+ return STRATEGIES . includes ( 'esm' ) && STRATEGIES . includes ( 'iife' ) ;
38+ }
39+
3640test . describe ( 'Unified element player strategy host' , ( ) => {
3741 test ( 'delivery uses one host for esm and iife' , async ( { page } ) => {
3842 test . setTimeout ( 120_000 ) ;
@@ -75,4 +79,201 @@ test.describe('Unified element player strategy host', () => {
7579 await page . waitForSelector ( `${ ELEMENT } -print` , { timeout : 45_000 } ) ;
7680 }
7781 } ) ;
82+
83+ test ( 'author forwards model updates as full snapshots' , async ( { page } ) => {
84+ test . setTimeout ( 120_000 ) ;
85+
86+ await page . goto ( `/${ ELEMENT } /author?player=esm` ) ;
87+ await page . waitForSelector ( 'pie-element-player[view="author"]' , { timeout : 45_000 } ) ;
88+ await waitForHostSettled ( page ) ;
89+ await page . waitForSelector ( `${ ELEMENT } -configure` , { timeout : 45_000 } ) ;
90+
91+ const marker = `marker-${ Date . now ( ) } ` ;
92+ const eventResult = await page . evaluate (
93+ async ( { elementName, markerValue } ) => {
94+ const host = document . querySelector ( 'pie-element-player' ) ;
95+ if ( ! ( host instanceof HTMLElement ) ) {
96+ return { ok : false , reason : 'host-missing' } ;
97+ }
98+ const configure = host . querySelector ( `${ elementName } -configure` ) as any ;
99+ if ( ! configure ) {
100+ return { ok : false , reason : 'configure-missing' } ;
101+ }
102+
103+ return await new Promise < { ok : boolean ; detail ?: any ; count ?: number ; reason ?: string } > (
104+ ( resolve ) => {
105+ let count = 0 ;
106+ let detail : unknown ;
107+ const listener = ( event : Event ) => {
108+ count += 1 ;
109+ detail = ( event as CustomEvent ) . detail ;
110+ } ;
111+ host . addEventListener ( 'model-changed' , listener ) ;
112+
113+ try {
114+ const baseModel =
115+ configure . model && typeof configure . model === 'object' ? configure . model : { } ;
116+ configure . model = { ...baseModel , parityMarker : markerValue } ;
117+ configure . dispatchEvent (
118+ new CustomEvent ( 'model.updated' , {
119+ detail : { update : { parityPartial : true , parityMarker : markerValue } } ,
120+ bubbles : false ,
121+ } )
122+ ) ;
123+ } catch {
124+ host . removeEventListener ( 'model-changed' , listener ) ;
125+ resolve ( { ok : false , reason : 'dispatch-failed' } ) ;
126+ return ;
127+ }
128+
129+ setTimeout ( ( ) => {
130+ host . removeEventListener ( 'model-changed' , listener ) ;
131+ resolve ( { ok : count > 0 , count, detail } ) ;
132+ } , 80 ) ;
133+ }
134+ ) ;
135+ } ,
136+ { elementName : ELEMENT , markerValue : marker }
137+ ) ;
138+
139+ expect ( eventResult . ok ) . toBeTruthy ( ) ;
140+ expect ( eventResult . count ) . toBeGreaterThan ( 0 ) ;
141+ expect ( eventResult . detail ) . toBeTruthy ( ) ;
142+ expect ( eventResult . detail ?. parityMarker ) . toBe ( marker ) ;
143+ } ) ;
144+
145+ test ( 'delivery forwards stable session payloads for repeated updates' , async ( { page } ) => {
146+ test . setTimeout ( 120_000 ) ;
147+
148+ await page . goto ( `/${ ELEMENT } /deliver?mode=gather&role=student&player=esm${ DEMO_QUERY } ` ) ;
149+ await page . waitForSelector ( 'pie-element-player[view="delivery"]' , { timeout : 45_000 } ) ;
150+ await waitForHostSettled ( page ) ;
151+
152+ const marker = `session-marker-${ Date . now ( ) } ` ;
153+ await page . evaluate (
154+ ( { elementName, markerValue } ) => {
155+ const host = document . querySelector ( 'pie-element-player' ) ;
156+ if ( ! ( host instanceof HTMLElement ) ) {
157+ return ;
158+ }
159+ const container = host . querySelector ( '.demo-element-player' ) ;
160+ const innerElement =
161+ container ?. querySelector < HTMLElement > ( `${ elementName } -element` ) ??
162+ container ?. querySelector < HTMLElement > ( ':scope > *:not(.loading):not(.error)' ) ??
163+ null ;
164+ if ( ! innerElement ) {
165+ return ;
166+ }
167+ ( window as any ) . __paritySessionDetails = [ ] ;
168+ host . addEventListener ( 'session-changed' , ( event : Event ) => {
169+ const detail = ( event as CustomEvent ) . detail ;
170+ ( window as any ) . __paritySessionDetails . push ( JSON . stringify ( detail ) ) ;
171+ } ) ;
172+ innerElement . dispatchEvent (
173+ new CustomEvent ( 'session-changed' , {
174+ detail : { parityMarker : markerValue , session : { value : [ 'A' ] } } ,
175+ bubbles : true ,
176+ composed : true ,
177+ } )
178+ ) ;
179+ innerElement . dispatchEvent (
180+ new CustomEvent ( 'session-changed' , {
181+ detail : { parityMarker : markerValue , session : { value : [ 'A' ] } } ,
182+ bubbles : true ,
183+ composed : true ,
184+ } )
185+ ) ;
186+ } ,
187+ { elementName : ELEMENT , markerValue : marker }
188+ ) ;
189+
190+ await page . waitForFunction ( ( ) => {
191+ const entries = ( window as any ) . __paritySessionDetails as string [ ] | undefined ;
192+ return Array . isArray ( entries ) && entries . length > 0 ;
193+ } ) ;
194+
195+ const eventResult = await page . evaluate ( ( ) => {
196+ const entries = ( ( window as any ) . __paritySessionDetails as string [ ] ) || [ ] ;
197+ let hasConsecutiveDuplicate = false ;
198+ for ( let i = 1 ; i < entries . length ; i += 1 ) {
199+ if ( entries [ i ] === entries [ i - 1 ] ) {
200+ hasConsecutiveDuplicate = true ;
201+ break ;
202+ }
203+ }
204+ const lastDetail = entries . length > 0 ? JSON . parse ( entries [ entries . length - 1 ] ) : null ;
205+ return {
206+ count : entries . length ,
207+ hasConsecutiveDuplicate,
208+ lastDetail,
209+ } ;
210+ } ) ;
211+
212+ expect ( eventResult . count ) . toBeGreaterThan ( 0 ) ;
213+ expect ( eventResult . lastDetail ?. session ) . toEqual ( { value : [ 'A' ] } ) ;
214+ } ) ;
215+
216+ test ( 'session remains stable across esm/iife strategy switches' , async ( { page } ) => {
217+ test . skip ( ! hasBothStrategies ( ) , 'Requires both esm and iife strategies' ) ;
218+ test . setTimeout ( 120_000 ) ;
219+
220+ const token = `switch-${ Date . now ( ) } ` ;
221+ await page . goto ( `/${ ELEMENT } /deliver?mode=gather&role=student&player=esm${ DEMO_QUERY } ` ) ;
222+ await page . waitForSelector ( 'pie-element-player[view="delivery"]' , { timeout : 45_000 } ) ;
223+ await waitForHostSettled ( page ) ;
224+
225+ await page . evaluate ( ( value ) => {
226+ const host = document . querySelector ( 'pie-element-player' ) ;
227+ if ( ! ( host instanceof HTMLElement ) ) return ;
228+ const container = host . querySelector ( '.demo-element-player' ) ;
229+ const innerElement =
230+ container ?. querySelector < HTMLElement > ( ':scope > *:not(.loading):not(.error)' ) ?? null ;
231+ if ( ! innerElement ) return ;
232+ const currentSession =
233+ ( host as any ) . session && typeof ( host as any ) . session === 'object'
234+ ? ( host as any ) . session
235+ : { } ;
236+ const nextSession = { ...currentSession , paritySwitchToken : value } ;
237+ innerElement . dispatchEvent (
238+ new CustomEvent ( 'session-changed' , {
239+ detail : { session : nextSession } ,
240+ bubbles : true ,
241+ composed : true ,
242+ } )
243+ ) ;
244+ } , token ) ;
245+
246+ await page . waitForFunction (
247+ ( value ) => {
248+ const host = document . querySelector ( 'pie-element-player' ) as any ;
249+ return host ?. session ?. paritySwitchToken === value ;
250+ } ,
251+ token ,
252+ { timeout : 15_000 }
253+ ) ;
254+
255+ await page . goto ( `/${ ELEMENT } /deliver?mode=gather&role=student&player=iife${ DEMO_QUERY } ` ) ;
256+ await page . waitForSelector ( 'pie-element-player[view="delivery"]' , { timeout : 45_000 } ) ;
257+ await waitForHostSettled ( page ) ;
258+ await page . waitForFunction (
259+ ( value ) => {
260+ const host = document . querySelector ( 'pie-element-player' ) as any ;
261+ return host ?. session ?. paritySwitchToken === value ;
262+ } ,
263+ token ,
264+ { timeout : 15_000 }
265+ ) ;
266+
267+ await page . goto ( `/${ ELEMENT } /deliver?mode=gather&role=student&player=esm${ DEMO_QUERY } ` ) ;
268+ await page . waitForSelector ( 'pie-element-player[view="delivery"]' , { timeout : 45_000 } ) ;
269+ await waitForHostSettled ( page ) ;
270+ await page . waitForFunction (
271+ ( value ) => {
272+ const host = document . querySelector ( 'pie-element-player' ) as any ;
273+ return host ?. session ?. paritySwitchToken === value ;
274+ } ,
275+ token ,
276+ { timeout : 15_000 }
277+ ) ;
278+ } ) ;
78279} ) ;
0 commit comments