File size: 4,288 Bytes
a0ebf39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { describe, it, expect, beforeEach, vi } from 'vitest';

const store: Record<string, string> = {};
const localStorageStub = {
  getItem: (k: string) => (k in store ? store[k] : null),
  setItem: (k: string, v: string) => {
    store[k] = String(v);
  },
  removeItem: (k: string) => {
    delete store[k];
  },
  clear: () => {
    for (const k of Object.keys(store)) delete store[k];
  },
  key: (i: number) => Object.keys(store)[i] ?? null,
  get length() {
    return Object.keys(store).length;
  },
};

vi.stubGlobal('localStorage', localStorageStub);
vi.stubGlobal('window', { localStorage: localStorageStub });

import {
  ANSWERS_KEY_PREFIX,
  DRAFT_KEY_PREFIX,
  RESULTS_KEY_PREFIX,
  clearAllForScene,
  clearSubmitted,
  readAnswersForSummary,
  readSubmittedState,
  writeSubmittedAnswers,
  writeSubmittedResults,
} from '@/lib/quiz/persistence';
import type { QuestionResult } from '@/lib/quiz/grading';

describe('quiz persistence', () => {
  beforeEach(() => {
    localStorageStub.clear();
  });

  it('readSubmittedState returns null when nothing is stored', () => {
    expect(readSubmittedState('s1')).toBeNull();
  });

  it('returns answering state when only answers are stored', () => {
    writeSubmittedAnswers('s1', { q1: 'a' });
    expect(readSubmittedState('s1')).toEqual({ kind: 'answering', answers: { q1: 'a' } });
  });

  it('returns reviewing state when both answers and results are stored', () => {
    const results: QuestionResult[] = [
      { questionId: 'q1', correct: true, status: 'correct', earned: 1 },
    ];
    writeSubmittedAnswers('s1', { q1: 'a' });
    writeSubmittedResults('s1', results);
    expect(readSubmittedState('s1')).toEqual({
      kind: 'reviewing',
      answers: { q1: 'a' },
      results,
    });
  });

  it('falls back to answering when results array is empty', () => {
    writeSubmittedAnswers('s1', { q1: 'a' });
    writeSubmittedResults('s1', []);
    expect(readSubmittedState('s1')).toEqual({ kind: 'answering', answers: { q1: 'a' } });
  });

  it('returns null on corrupt answers JSON', () => {
    localStorageStub.setItem(ANSWERS_KEY_PREFIX + 's1', '{invalid');
    expect(readSubmittedState('s1')).toBeNull();
  });

  it('clearSubmitted wipes answers + results but leaves draft intact', () => {
    localStorageStub.setItem(DRAFT_KEY_PREFIX + 's1', JSON.stringify({ q1: 'b' }));
    writeSubmittedAnswers('s1', { q1: 'a' });
    writeSubmittedResults('s1', [
      { questionId: 'q1', correct: true, status: 'correct', earned: 1 },
    ]);

    clearSubmitted('s1');

    expect(readSubmittedState('s1')).toBeNull();
    expect(localStorageStub.getItem(DRAFT_KEY_PREFIX + 's1')).not.toBeNull();
  });

  it('clearAllForScene wipes all three keys for a single scene', () => {
    localStorageStub.setItem(DRAFT_KEY_PREFIX + 's1', '{}');
    writeSubmittedAnswers('s1', { q1: 'a' });
    writeSubmittedResults('s1', [
      { questionId: 'q1', correct: true, status: 'correct', earned: 1 },
    ]);
    // unrelated scene should not be touched
    writeSubmittedAnswers('s2', { q1: 'z' });

    clearAllForScene('s1');

    expect(localStorageStub.getItem(DRAFT_KEY_PREFIX + 's1')).toBeNull();
    expect(localStorageStub.getItem(ANSWERS_KEY_PREFIX + 's1')).toBeNull();
    expect(localStorageStub.getItem(RESULTS_KEY_PREFIX + 's1')).toBeNull();
    expect(localStorageStub.getItem(ANSWERS_KEY_PREFIX + 's2')).not.toBeNull();
  });

  it('readAnswersForSummary prefers submitted over draft', () => {
    localStorageStub.setItem(DRAFT_KEY_PREFIX + 's1', JSON.stringify({ q1: 'draft' }));
    writeSubmittedAnswers('s1', { q1: 'submitted' });
    expect(readAnswersForSummary('s1')).toEqual({ q1: 'submitted' });
  });

  it('readAnswersForSummary falls back to draft when no submission', () => {
    localStorageStub.setItem(DRAFT_KEY_PREFIX + 's1', JSON.stringify({ q1: 'draft' }));
    expect(readAnswersForSummary('s1')).toEqual({ q1: 'draft' });
  });

  it('readAnswersForSummary returns empty object when nothing is stored', () => {
    expect(readAnswersForSummary('s1')).toEqual({});
  });

  it('tolerates corrupt draft JSON when no submission exists', () => {
    localStorageStub.setItem(DRAFT_KEY_PREFIX + 's1', '{corrupt');
    expect(readAnswersForSummary('s1')).toEqual({});
  });
});