+ {{ completedStopName }} +
+ ++ {{ completedStopName }} +
+ ++ {{ t('success.next_stop_label') }} +
++ {{ nextStop.title }} +
+{{ errorMessage }}
-{{ submitErrorMessage }}
++ {{ result.passed ? $t('stop.correct') : $t('stop.incorrect') }} +
+{{ result.score }} / {{ result.maxScore }}
++ {{ feedbackText }} +
+ ++ {{ $t('stop.explanation') }}: + {{ option.explanationText?.[lang] }} +
+ + + diff --git a/src/stops/model/__tests__/stop.store.spec.ts b/src/stops/model/__tests__/stop.store.spec.ts index 2c9f55b5..950227a1 100644 --- a/src/stops/model/__tests__/stop.store.spec.ts +++ b/src/stops/model/__tests__/stop.store.spec.ts @@ -26,7 +26,7 @@ const taskSummary = { difficultyLevel: 1, taskType: 'NEWS_COMPARISON', published: true, - itemCount: 2, + itemCount: 1, updatedAt: null, } @@ -47,18 +47,26 @@ const fullTask = { { id: 1, itemOrder: 1, - promptText: { nb: 'Artikkel A', en: 'Article A' }, + promptText: { nb: 'Hvilken artikkel er troverdig?', en: 'Which article is trustworthy?' }, mediaUrl: null, - metadataJson: '{"domain":"forskning.no"}', - options: [], - }, - { - id: 2, - itemOrder: 2, - promptText: { nb: 'Artikkel B', en: 'Article B' }, - mediaUrl: null, - metadataJson: '{"domain":"klikk.no"}', - options: [], + metadataJson: + '{"articles":[{"optionOrder":1,"domain":"forskning.no","headline":{"nb":"Artikkel A","en":"Article A"},"body":{"nb":"Brødtekst A","en":"Body A"},"mediaUrl":null},{"optionOrder":2,"domain":"klikk.no","headline":{"nb":"Artikkel B","en":"Article B"},"body":{"nb":"Brødtekst B","en":"Body B"},"mediaUrl":null}]}', + options: [ + { + id: 11, + optionOrder: 1, + optionText: { nb: 'Artikkel A', en: 'Article A' }, + isCorrect: null, + explanationText: null, + }, + { + id: 12, + optionOrder: 2, + optionText: { nb: 'Artikkel B', en: 'Article B' }, + isCorrect: null, + explanationText: null, + }, + ], }, ], } @@ -131,7 +139,7 @@ describe('useStopStore', () => { expect(stopStore.status).toBe('loaded') expect(stopStore.tasks).toHaveLength(1) expect(stopStore.tasks[0]?.id).toBe(10) - expect(stopStore.tasks[0]?.items).toHaveLength(2) + expect(stopStore.tasks[0]?.items).toHaveLength(1) }) it('fetches map overview first when it has not been loaded yet', async () => { @@ -152,37 +160,30 @@ describe('useStopStore', () => { expect(fetchMock).toHaveBeenCalledTimes(3) }) - it('sets status to error when not authenticated', async () => { + it('omits unpublished tasks from the loaded list', async () => { + seedAuthStore() + seedMapStore() + + const unpublishedSummary = { ...taskSummary, id: 99, published: false } + vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(jsonResponse([unpublishedSummary]))) + const stopStore = useStopStore() await stopStore.loadTasksForStop('nyhetskvartalet') - expect(stopStore.status).toBe('error') - expect(stopStore.errorMessage).toBe('Not authenticated.') + expect(stopStore.tasks).toHaveLength(0) + expect(vi.mocked(fetch)).toHaveBeenCalledOnce() }) - it('sets status to error when the current session is not a pupil session', async () => { - const authStore = useAuthStore() - authStore.accessToken = 'access-token' - authStore.user = { - subject: 'teacher-1', - portalType: 'TEACHER', - email: 'teacher@example.com', - username: null, - authorities: ['ROLE_TEACHER'], - pupilProfile: null, - teacherProfile: null, - } - + it('sets status to error when not authenticated', async () => { const stopStore = useStopStore() await stopStore.loadTasksForStop('nyhetskvartalet') expect(stopStore.status).toBe('error') - expect(stopStore.errorMessage).toBe('Stop access is only available for pupil sessions.') + expect(stopStore.errorMessage).toBe('Not authenticated.') }) it('sets status to error when the slug is not found after fetching map overview', async () => { seedAuthStore() - vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(jsonResponse([]))) const stopStore = useStopStore() @@ -197,7 +198,9 @@ describe('useStopStore', () => { seedMapStore() vi.stubGlobal( 'fetch', - vi.fn().mockResolvedValueOnce(jsonResponse({ detail: 'Forbidden' }, { status: 403 })), + vi.fn().mockResolvedValueOnce( + jsonResponse({ detail: 'Forbidden' }, { status: 403 }), + ), ) const stopStore = useStopStore() @@ -207,14 +210,57 @@ describe('useStopStore', () => { expect(stopStore.errorMessage).toBe('Forbidden') }) - it('records the selected itemId for a task', () => { + it('clears previous tasks when a new load starts', async () => { + seedAuthStore() + seedMapStore() + + vi.stubGlobal( + 'fetch', + vi.fn() + .mockResolvedValueOnce(jsonResponse([taskSummary])) + .mockResolvedValueOnce(jsonResponse(fullTask)), + ) + + const stopStore = useStopStore() + await stopStore.loadTasksForStop('nyhetskvartalet') + expect(stopStore.tasks).toHaveLength(1) + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValueOnce(jsonResponse({ detail: 'Forbidden' }, { status: 403 })), + ) + await stopStore.loadTasksForStop('nyhetskvartalet') + + expect(stopStore.tasks).toHaveLength(0) + }) + + it('records the selected option for a task item', () => { + const stopStore = useStopStore() + stopStore.setAnswer(10, 1, 12) + + expect(stopStore.answers.get(10)?.get(1)).toBe(12) + }) + + it('replaces a previous answer for the same task item', () => { + const stopStore = useStopStore() + stopStore.setAnswer(10, 1, 11) + stopStore.setAnswer(10, 1, 12) + + expect(stopStore.answers.get(10)?.get(1)).toBe(12) + expect(stopStore.answers.size).toBe(1) + }) + + it('tracks answers for multiple tasks independently', () => { const stopStore = useStopStore() - stopStore.setAnswer(10, 2) + stopStore.setAnswer(10, 1, 11) + stopStore.setAnswer(11, 2, 24) - expect(stopStore.answers.get(10)).toBe(2) + expect(stopStore.answers.get(10)?.get(1)).toBe(11) + expect(stopStore.answers.get(11)?.get(2)).toBe(24) + expect(stopStore.answers.size).toBe(2) }) - it('reset clears tasks, answers, and error state', async () => { + it('clears tasks, answers, and error state on reset', async () => { seedAuthStore() seedMapStore() @@ -227,7 +273,7 @@ describe('useStopStore', () => { const stopStore = useStopStore() await stopStore.loadTasksForStop('nyhetskvartalet') - stopStore.setAnswer(1, 1) + stopStore.setAnswer(1, 1, 11) stopStore.reset() diff --git a/src/stops/model/stop.store.ts b/src/stops/model/stop.store.ts index 6b69e2b4..57ad5e60 100644 --- a/src/stops/model/stop.store.ts +++ b/src/stops/model/stop.store.ts @@ -2,10 +2,15 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { useAuthStore } from '@/auth/model/auth.store' import { useMapStore } from '@/map/model/map.store' -import { requestTask, requestTasksForStop } from '@/stops/api/stops.api' -import type { TaskResponse } from '@/stops/api/stops.api' +import { requestSubmitStopAnswers, requestTask, requestTasksForStop } from '@/stops/api/stops.api' +import type { + StopSubmissionResponse, + TaskResponse, + TaskSubmissionResultResponse, +} from '@/stops/api/stops.api' export type StopLoadStatus = 'idle' | 'loading' | 'loaded' | 'error' +export type StopSubmitStatus = 'idle' | 'submitting' | 'submitted' | 'error' export const useStopStore = defineStore('stop', () => { const authStore = useAuthStore() @@ -14,8 +19,11 @@ export const useStopStore = defineStore('stop', () => { const tasks = ref