@@ -235,3 +235,161 @@ describe('markPlayerAway', () => {
235235 expect ( markPlayerAway ( [ ] , 'p1' ) ) . toHaveLength ( 0 )
236236 } )
237237} )
238+
239+ // ── buildNavSequence ──────────────────────────────────────────────────────────
240+
241+ import { buildNavSequence } from '@/pages/admin/gamemaster-utils'
242+ import type { GameQuestion , Round } from '@/db'
243+
244+ const ROUNDS : Round [ ] = [
245+ { id : 'r1' , name : 'Round 1' , description : '' , questionIds : [ 'q1' , 'q2' ] , createdAt : 0 } ,
246+ { id : 'r2' , name : 'Round 2' , description : '' , questionIds : [ 'q3' ] , createdAt : 0 } ,
247+ ]
248+
249+ const GQS : GameQuestion [ ] = [
250+ { id : 'gq1' , gameId : 'g1' , questionId : 'q1' , roundId : 'r1' , order : 0 , status : 'pending' } ,
251+ { id : 'gq2' , gameId : 'g1' , questionId : 'q2' , roundId : 'r1' , order : 1 , status : 'correct' } ,
252+ { id : 'gq3' , gameId : 'g1' , questionId : 'q3' , roundId : 'r2' , order : 2 , status : 'pending' } ,
253+ ]
254+
255+ describe ( 'buildNavSequence' , ( ) => {
256+ it ( 'returns one entry per game question' , ( ) => {
257+ expect ( buildNavSequence ( GQS , ROUNDS ) ) . toHaveLength ( 3 )
258+ } )
259+
260+ it ( 'assigns flat indices 0, 1, 2' , ( ) => {
261+ const seq = buildNavSequence ( GQS , ROUNDS )
262+ expect ( seq . map ( e => e . flatIndex ) ) . toEqual ( [ 0 , 1 , 2 ] )
263+ } )
264+
265+ it ( 'resolves round name and index from rounds list' , ( ) => {
266+ const seq = buildNavSequence ( GQS , ROUNDS )
267+ expect ( seq [ 0 ] . roundName ) . toBe ( 'Round 1' )
268+ expect ( seq [ 0 ] . roundIdx ) . toBe ( 0 )
269+ expect ( seq [ 2 ] . roundName ) . toBe ( 'Round 2' )
270+ expect ( seq [ 2 ] . roundIdx ) . toBe ( 1 )
271+ } )
272+
273+ it ( 'carries questionId and gameQuestionId' , ( ) => {
274+ const seq = buildNavSequence ( GQS , ROUNDS )
275+ expect ( seq [ 0 ] . questionId ) . toBe ( 'q1' )
276+ expect ( seq [ 0 ] . gameQuestionId ) . toBe ( 'gq1' )
277+ } )
278+
279+ it ( 'carries question status' , ( ) => {
280+ const seq = buildNavSequence ( GQS , ROUNDS )
281+ expect ( seq [ 1 ] . questionStatus ) . toBe ( 'correct' )
282+ } )
283+
284+ it ( 'sorts by order field regardless of input order' , ( ) => {
285+ const shuffled = [ GQS [ 2 ] , GQS [ 0 ] , GQS [ 1 ] ]
286+ const seq = buildNavSequence ( shuffled , ROUNDS )
287+ expect ( seq . map ( e => e . questionId ) ) . toEqual ( [ 'q1' , 'q2' , 'q3' ] )
288+ } )
289+
290+ it ( 'returns empty array for no questions' , ( ) => {
291+ expect ( buildNavSequence ( [ ] , ROUNDS ) ) . toHaveLength ( 0 )
292+ } )
293+
294+ it ( 'falls back round name to "Round" for unknown roundId' , ( ) => {
295+ const gqOrphan : GameQuestion = {
296+ id : 'gq9' ,
297+ gameId : 'g1' ,
298+ questionId : 'qX' ,
299+ roundId : 'unknown' ,
300+ order : 0 ,
301+ status : 'pending' ,
302+ }
303+ const seq = buildNavSequence ( [ gqOrphan ] , ROUNDS )
304+ expect ( seq [ 0 ] . roundName ) . toBe ( 'Round' )
305+ expect ( seq [ 0 ] . roundIdx ) . toBe ( 0 )
306+ } )
307+
308+ it ( 'does not mutate the input array' , ( ) => {
309+ const input = [ ...GQS ]
310+ buildNavSequence ( [ GQS [ 2 ] , GQS [ 0 ] ] , ROUNDS )
311+ expect ( input ) . toHaveLength ( 3 )
312+ } )
313+ } )
314+
315+ // ── getNavPosition + step ─────────────────────────────────────────────────────
316+
317+ import { getNavPosition , step } from '@/pages/admin/gamemaster-utils'
318+
319+ // Reuse GQS + ROUNDS from above — 3 questions: r1(q1,q2), r2(q3)
320+ const SEQ = buildNavSequence ( GQS , ROUNDS )
321+
322+ describe ( 'getNavPosition' , ( ) => {
323+ it ( 'first question: isFirst=true, isLast=false, roundIdx=0' , ( ) => {
324+ const pos = getNavPosition ( SEQ , 0 , - 1 )
325+ expect ( pos . isFirst ) . toBe ( true )
326+ expect ( pos . isLast ) . toBe ( false )
327+ expect ( pos . roundIdx ) . toBe ( 0 )
328+ } )
329+
330+ it ( 'last question: isLast=true' , ( ) => {
331+ const pos = getNavPosition ( SEQ , 2 , 1 )
332+ expect ( pos . isLast ) . toBe ( true )
333+ expect ( pos . isFirst ) . toBe ( false )
334+ } )
335+
336+ it ( 'questionIdx is position within the round' , ( ) => {
337+ expect ( getNavPosition ( SEQ , 0 , - 1 ) . questionIdx ) . toBe ( 0 ) // q1 is 1st in r1
338+ expect ( getNavPosition ( SEQ , 1 , - 1 ) . questionIdx ) . toBe ( 1 ) // q2 is 2nd in r1
339+ expect ( getNavPosition ( SEQ , 2 , - 1 ) . questionIdx ) . toBe ( 0 ) // q3 is 1st in r2
340+ } )
341+
342+ it ( 'roundQuestions reflects count in that round' , ( ) => {
343+ expect ( getNavPosition ( SEQ , 0 , - 1 ) . roundQuestions ) . toBe ( 2 ) // r1 has 2
344+ expect ( getNavPosition ( SEQ , 2 , - 1 ) . roundQuestions ) . toBe ( 1 ) // r2 has 1
345+ } )
346+
347+ it ( 'isRoundBoundary false on first call (prevRoundIdx = -1)' , ( ) => {
348+ expect ( getNavPosition ( SEQ , 0 , - 1 ) . isRoundBoundary ) . toBe ( false )
349+ } )
350+
351+ it ( 'isRoundBoundary false when staying in same round' , ( ) => {
352+ expect ( getNavPosition ( SEQ , 1 , 0 ) . isRoundBoundary ) . toBe ( false ) // r1 → r1
353+ } )
354+
355+ it ( 'isRoundBoundary true when crossing into next round' , ( ) => {
356+ expect ( getNavPosition ( SEQ , 2 , 0 ) . isRoundBoundary ) . toBe ( true ) // r1 → r2
357+ } )
358+
359+ it ( 'isRoundBoundary true when going backwards across a round' , ( ) => {
360+ // Moving from flatIndex=2 (r2) backward to flatIndex=1 (r1) — boundary crossed
361+ expect ( getNavPosition ( SEQ , 1 , 1 ) . isRoundBoundary ) . toBe ( true )
362+ // Moving within r1 backward — no boundary
363+ expect ( getNavPosition ( SEQ , 0 , 0 ) . isRoundBoundary ) . toBe ( false )
364+ } )
365+
366+ it ( 'returns flatIndex matching the requested index' , ( ) => {
367+ expect ( getNavPosition ( SEQ , 1 , 0 ) . flatIndex ) . toBe ( 1 )
368+ } )
369+ } )
370+
371+ describe ( 'step' , ( ) => {
372+ it ( 'moves forward by 1' , ( ) => {
373+ expect ( step ( SEQ , 0 , 1 ) ) . toBe ( 1 )
374+ expect ( step ( SEQ , 1 , 1 ) ) . toBe ( 2 )
375+ } )
376+
377+ it ( 'moves backward by 1' , ( ) => {
378+ expect ( step ( SEQ , 2 , - 1 ) ) . toBe ( 1 )
379+ expect ( step ( SEQ , 1 , - 1 ) ) . toBe ( 0 )
380+ } )
381+
382+ it ( 'clamps at 0 when going back from first' , ( ) => {
383+ expect ( step ( SEQ , 0 , - 1 ) ) . toBe ( 0 )
384+ } )
385+
386+ it ( 'clamps at last when going forward from last' , ( ) => {
387+ expect ( step ( SEQ , 2 , 1 ) ) . toBe ( 2 )
388+ } )
389+
390+ it ( 'handles single-question sequence' , ( ) => {
391+ const single = [ SEQ [ 0 ] ]
392+ expect ( step ( single , 0 , 1 ) ) . toBe ( 0 )
393+ expect ( step ( single , 0 , - 1 ) ) . toBe ( 0 )
394+ } )
395+ } )
0 commit comments