11import { act , render , screen , waitFor } from '@testing-library/react' ;
22import { getAuthStatus , subscribeToAuthStateChange } from '../lib/identity' ;
33import { useMatchMedia } from '../lib/useMatchMedia' ;
4+ import { puzzleConfigs } from '../model/puzzles/puzzleConfigs' ;
45import {
56 buildPuzzleIframeSrc ,
67 type PuzzleContext ,
@@ -35,79 +36,65 @@ const signedOut = () => ({ kind: 'SignedOut' as const });
3536const contextParam = ( context : PuzzleContext ) =>
3637 `guardian-puzzle-context=${ encodeURIComponent ( JSON . stringify ( context ) ) } ` ;
3738
39+ const sudokuEasyConfig = puzzleConfigs [ 'sudoku-easy' ] ! ;
40+ const wordiplyConfig = puzzleConfigs . wordiply ! ;
41+
3842describe ( 'buildPuzzleIframeSrc' , ( ) => {
39- it ( 'appends the context as a JSON query param when the src has none ' , ( ) => {
43+ it ( 'resolves the provider URL then appends guardian-puzzle-context on top ' , ( ) => {
4044 const context : PuzzleContext = {
4145 userId : null ,
4246 darkMode : false ,
4347 puzzleDate : null ,
4448 } ;
45- expect (
46- buildPuzzleIframeSrc ( ' https://example. com/puzzle' , context ) ,
47- ) . toBe ( `https://example.com/puzzle? ${ contextParam ( context ) } ` ) ;
49+ expect ( buildPuzzleIframeSrc ( sudokuEasyConfig , context ) ) . toBe (
50+ ` https://tg.amuselabs. com/guardian/date-picker?set=guardian-sudoku-easy&embed=1&idx=1&darkMode=0& ${ contextParam ( context ) } ` ,
51+ ) ;
4852 } ) ;
4953
50- it ( 'preserves existing query params when appending the context' , ( ) => {
54+ it ( 'preserves the provider-resolved uid/darkMode params when appending guardian-puzzle- context' , ( ) => {
5155 const context : PuzzleContext = {
5256 userId : 'abc123' ,
5357 darkMode : true ,
5458 puzzleDate : '2026-09-15' ,
5559 } ;
56- expect (
57- buildPuzzleIframeSrc (
58- 'https://example.com/puzzle?set=guardian-sudoku-easy&embed=1' ,
59- context ,
60- ) ,
61- ) . toBe (
62- `https://example.com/puzzle?set=guardian-sudoku-easy&embed=1& ${ contextParam ( context ) } &uid=abc123` ,
60+ const src = buildPuzzleIframeSrc ( sudokuEasyConfig , context ) ;
61+ const url = new URL ( src ) ;
62+
63+ expect ( url . searchParams . get ( 'uid' ) ) . toBe ( 'abc123' ) ;
64+ expect ( url . searchParams . get ( 'darkMode' ) ) . toBe ( '1' ) ;
65+ expect ( url . searchParams . get ( 'guardian-puzzle-context' ) ) . toBe (
66+ JSON . stringify ( context ) ,
6367 ) ;
6468 } ) ;
6569
66- it ( 'always includes the context, even when signed out and dark mode is off' , ( ) => {
70+ it ( 'always includes guardian-puzzle- context, even when signed out and dark mode is off' , ( ) => {
6771 const context : PuzzleContext = {
6872 userId : null ,
6973 darkMode : false ,
7074 puzzleDate : null ,
7175 } ;
72- expect (
73- buildPuzzleIframeSrc ( 'https://example.com/puzzle' , context ) ,
74- ) . toContain ( 'guardian-puzzle-context=' ) ;
75- } ) ;
76-
77- it ( 'returns the src unchanged if it cannot be parsed as an absolute URL' , ( ) => {
78- expect (
79- buildPuzzleIframeSrc ( 'not-a-url' , {
80- userId : null ,
81- darkMode : false ,
82- puzzleDate : null ,
83- } ) ,
84- ) . toBe ( 'not-a-url' ) ;
85- } ) ;
86-
87- it ( 'appends uid alongside guardian-puzzle-context when the reader is signed in' , ( ) => {
88- const context : PuzzleContext = {
89- userId : 'user-123' ,
90- darkMode : false ,
91- puzzleDate : null ,
92- } ;
93- const src = buildPuzzleIframeSrc ( 'https://example.com/puzzle' , context ) ;
94- const url = new URL ( src ) ;
95-
96- expect ( url . searchParams . get ( 'uid' ) ) . toBe ( 'user-123' ) ;
97- expect ( url . searchParams . has ( 'guardian-puzzle-context' ) ) . toBe ( true ) ;
76+ expect ( buildPuzzleIframeSrc ( sudokuEasyConfig , context ) ) . toContain (
77+ 'guardian-puzzle-context=' ,
78+ ) ;
9879 } ) ;
9980
100- it ( 'omits uid entirely when the reader is signed out (not uid=null or empty )' , ( ) => {
81+ it ( 'applies guardian-puzzle-context uniformly to a non-AmuseLabs provider too (wordiply )' , ( ) => {
10182 const context : PuzzleContext = {
102- userId : null ,
103- darkMode : false ,
83+ userId : 'abc123' ,
84+ darkMode : true ,
10485 puzzleDate : null ,
10586 } ;
106- const src = buildPuzzleIframeSrc ( 'https://example.com/puzzle' , context ) ;
87+ const src = buildPuzzleIframeSrc ( wordiplyConfig , context ) ;
10788 const url = new URL ( src ) ;
10889
90+ expect ( url . origin + url . pathname ) . toBe ( 'https://www.wordiply.com/' ) ;
91+ expect ( url . searchParams . get ( 'guardian-puzzle-context' ) ) . toBe (
92+ JSON . stringify ( context ) ,
93+ ) ;
94+ // Wordiply has no confirmed uid/darkMode query param support, so
95+ // neither is provider-added, only DCR's own generic context blob is.
10996 expect ( url . searchParams . has ( 'uid' ) ) . toBe ( false ) ;
110- expect ( url . searchParams . has ( 'guardian-puzzle-context ' ) ) . toBe ( true ) ;
97+ expect ( url . searchParams . has ( 'darkMode ' ) ) . toBe ( false ) ;
11198 } ) ;
11299} ) ;
113100
@@ -131,12 +118,15 @@ describe('PuzzleIframe', () => {
131118 const getUidFromSrc = ( src : string ) : string | null =>
132119 new URL ( src ) . searchParams . get ( 'uid' ) ;
133120
121+ const getDarkModeParamFromSrc = ( src : string ) : string | null =>
122+ new URL ( src ) . searchParams . get ( 'darkMode' ) ;
123+
134124 it ( 'renders userId: null and darkMode: false while signed out with dark mode unavailable' , async ( ) => {
135125 mockedGetAuthStatus . mockResolvedValue ( signedOut ( ) ) ;
136126
137127 render (
138128 < PuzzleIframe
139- src = "https://example.com/puzzle"
129+ puzzleConfig = { sudokuEasyConfig }
140130 title = "Puzzle"
141131 darkModeAvailable = { false }
142132 puzzleDate = { null }
@@ -158,7 +148,7 @@ describe('PuzzleIframe', () => {
158148
159149 render (
160150 < PuzzleIframe
161- src = "https://example.com/puzzle"
151+ puzzleConfig = { sudokuEasyConfig }
162152 title = "Puzzle"
163153 darkModeAvailable = { false }
164154 puzzleDate = { null }
@@ -181,7 +171,7 @@ describe('PuzzleIframe', () => {
181171
182172 render (
183173 < PuzzleIframe
184- src = "https://example.com/puzzle"
174+ puzzleConfig = { sudokuEasyConfig }
185175 title = "Puzzle"
186176 darkModeAvailable = { false }
187177 puzzleDate = { null }
@@ -200,7 +190,7 @@ describe('PuzzleIframe', () => {
200190
201191 render (
202192 < PuzzleIframe
203- src = "https://example.com/puzzle"
193+ puzzleConfig = { sudokuEasyConfig }
204194 title = "Puzzle"
205195 darkModeAvailable = { true }
206196 puzzleDate = { null }
@@ -219,7 +209,7 @@ describe('PuzzleIframe', () => {
219209
220210 const { rerender } = render (
221211 < PuzzleIframe
222- src = "https://example.com/puzzle"
212+ puzzleConfig = { sudokuEasyConfig }
223213 title = "Puzzle"
224214 darkModeAvailable = { true }
225215 puzzleDate = { null }
@@ -239,7 +229,7 @@ describe('PuzzleIframe', () => {
239229 mockedUseMatchMedia . mockReturnValue ( true ) ;
240230 rerender (
241231 < PuzzleIframe
242- src = "https://example.com/puzzle"
232+ puzzleConfig = { sudokuEasyConfig }
243233 title = "Puzzle"
244234 darkModeAvailable = { true }
245235 puzzleDate = { null }
@@ -257,7 +247,7 @@ describe('PuzzleIframe', () => {
257247
258248 render (
259249 < PuzzleIframe
260- src = "https://example.com/puzzle"
250+ puzzleConfig = { sudokuEasyConfig }
261251 title = "Puzzle"
262252 darkModeAvailable = { true }
263253 puzzleDate = { null }
@@ -293,7 +283,7 @@ describe('PuzzleIframe', () => {
293283
294284 render (
295285 < PuzzleIframe
296- src = "https://example.com/puzzle"
286+ puzzleConfig = { sudokuEasyConfig }
297287 title = "Puzzle"
298288 darkModeAvailable = { false }
299289 puzzleDate = { null }
@@ -324,7 +314,7 @@ describe('PuzzleIframe', () => {
324314
325315 const { unmount } = render (
326316 < PuzzleIframe
327- src = "https://example.com/puzzle"
317+ puzzleConfig = { sudokuEasyConfig }
328318 title = "Puzzle"
329319 darkModeAvailable = { false }
330320 puzzleDate = { null }
@@ -342,7 +332,7 @@ describe('PuzzleIframe', () => {
342332
343333 render (
344334 < PuzzleIframe
345- src = "https://example.com/puzzle"
335+ puzzleConfig = { sudokuEasyConfig }
346336 title = "Puzzle"
347337 darkModeAvailable = { false }
348338 puzzleDate = "2026-09-15"
@@ -360,7 +350,7 @@ describe('PuzzleIframe', () => {
360350
361351 render (
362352 < PuzzleIframe
363- src = "https://example.com/puzzle"
353+ puzzleConfig = { sudokuEasyConfig }
364354 title = "Puzzle"
365355 darkModeAvailable = { false }
366356 puzzleDate = { null }
@@ -373,31 +363,33 @@ describe('PuzzleIframe', () => {
373363 ) ;
374364 } ) ;
375365
376- it ( 'includes uid alongside guardian-puzzle-context once signed in' , async ( ) => {
366+ it ( 'includes uid and darkMode=1 (AmuseLabs-specific) alongside guardian-puzzle-context once signed in with dark mode on ' , async ( ) => {
377367 mockedGetAuthStatus . mockResolvedValue ( signedIn ( 'user-123' ) ) ;
368+ mockedUseMatchMedia . mockReturnValue ( true ) ;
378369
379370 render (
380371 < PuzzleIframe
381- src = "https://example.com/puzzle"
372+ puzzleConfig = { sudokuEasyConfig }
382373 title = "Puzzle"
383- darkModeAvailable = { false }
374+ darkModeAvailable = { true }
384375 puzzleDate = { null }
385376 /> ,
386377 ) ;
387378
388379 const iframe = await screen . findByTitle < HTMLIFrameElement > ( 'Puzzle' ) ;
389380 await waitFor ( ( ) => expect ( getUidFromSrc ( iframe . src ) ) . toBe ( 'user-123' ) ) ;
381+ expect ( getDarkModeParamFromSrc ( iframe . src ) ) . toBe ( '1' ) ;
390382 expect (
391383 new URL ( iframe . src ) . searchParams . has ( 'guardian-puzzle-context' ) ,
392384 ) . toBe ( true ) ;
393385 } ) ;
394386
395- it ( 'omits uid entirely while signed out (not uid=null or empty )' , async ( ) => {
387+ it ( 'omits uid entirely while signed out, but still sends darkMode=0 (AmuseLabs-specific )' , async ( ) => {
396388 mockedGetAuthStatus . mockResolvedValue ( signedOut ( ) ) ;
397389
398390 render (
399391 < PuzzleIframe
400- src = "https://example.com/puzzle"
392+ puzzleConfig = { sudokuEasyConfig }
401393 title = "Puzzle"
402394 darkModeAvailable = { false }
403395 puzzleDate = { null }
@@ -409,6 +401,7 @@ describe('PuzzleIframe', () => {
409401 expect ( getContextFromSrc ( iframe . src ) . userId ) . toBeNull ( ) ,
410402 ) ;
411403 expect ( new URL ( iframe . src ) . searchParams . has ( 'uid' ) ) . toBe ( false ) ;
404+ expect ( getDarkModeParamFromSrc ( iframe . src ) ) . toBe ( '0' ) ;
412405 expect (
413406 new URL ( iframe . src ) . searchParams . has ( 'guardian-puzzle-context' ) ,
414407 ) . toBe ( true ) ;
@@ -419,7 +412,7 @@ describe('PuzzleIframe', () => {
419412
420413 render (
421414 < PuzzleIframe
422- src = "https://example.com/puzzle"
415+ puzzleConfig = { sudokuEasyConfig }
423416 title = "Puzzle"
424417 darkModeAvailable = { false }
425418 puzzleDate = { null }
@@ -438,4 +431,28 @@ describe('PuzzleIframe', () => {
438431
439432 await waitFor ( ( ) => expect ( getUidFromSrc ( iframe . src ) ) . toBeNull ( ) ) ;
440433 } ) ;
434+
435+ it ( 'does not add uid/darkMode query params for a non-AmuseLabs provider (wordiply)' , async ( ) => {
436+ mockedGetAuthStatus . mockResolvedValue ( signedIn ( 'user-123' ) ) ;
437+ mockedUseMatchMedia . mockReturnValue ( true ) ;
438+
439+ render (
440+ < PuzzleIframe
441+ puzzleConfig = { wordiplyConfig }
442+ title = "Puzzle"
443+ darkModeAvailable = { true }
444+ puzzleDate = { null }
445+ /> ,
446+ ) ;
447+
448+ const iframe = await screen . findByTitle < HTMLIFrameElement > ( 'Puzzle' ) ;
449+ await waitFor ( ( ) =>
450+ expect (
451+ new URL ( iframe . src ) . searchParams . has ( 'guardian-puzzle-context' ) ,
452+ ) . toBe ( true ) ,
453+ ) ;
454+ expect ( new URL ( iframe . src ) . searchParams . has ( 'uid' ) ) . toBe ( false ) ;
455+ expect ( new URL ( iframe . src ) . searchParams . has ( 'darkMode' ) ) . toBe ( false ) ;
456+ expect ( iframe . src . startsWith ( 'https://www.wordiply.com/' ) ) . toBe ( true ) ;
457+ } ) ;
441458} ) ;
0 commit comments