Files
gold_land_user/test/mutation-queue.test.ts
T
safavimohadeseh78andCursor e3f0457dc9
Frontend CI / unit (test, type-check, build) (push) Successful in 1m11s
Frontend CI / browser E2E (frontend + pinned backend) (push) Failing after 2m48s
feat: complete server-backed six-lesson app and trial tooling
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 14:40:37 +03:30

110 lines
3.8 KiB
TypeScript

/**
* Mutation queue — server-freshness vs response arrival order (same tab).
*/
import assert from 'node:assert/strict'
import test from 'node:test'
import { createPinia, setActivePinia } from 'pinia'
import { createMeFixture } from './fixtures/me.fixture'
import { installFetchMock, resetAppTestState } from './helpers/fetchMock'
test.beforeEach(async () => {
installFetchMock()
await resetAppTestState()
setActivePinia(createPinia())
const { resetUserStores } = await import('../src/api/sync')
resetUserStores()
})
test('uncoordinated late applyMe simulates stale server overwrite regression', async () => {
const { applyMeResponse } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
applyMeResponse(createMeFixture({ coins: 10 }))
const newer = createMeFixture({ coins: 200 })
const older = createMeFixture({ coins: 50 })
applyMeResponse(newer)
applyMeResponse(older)
assert.equal(useUserStore().coins, 50, 'older server snapshot must not win when applied last')
})
test('serial queue: slower older server response then newer mutation keeps final newer state', async () => {
const meOlder = createMeFixture({ coins: 50 })
const meNewer = createMeFixture({ coins: 200 })
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
const { api } = await import('../src/api/client')
applyMeResponse(createMeFixture({ coins: 10 }))
let call = 0
globalThis.routeHandlers!.set('/store/purchases', () => {
call += 1
if (call === 1) {
return { status: 200, body: meOlder, delayMs: 35 }
}
return { status: 200, body: meNewer }
})
const first = runMeMutation(() => api.purchaseProduct('castle'))
const second = runMeMutation(() => api.purchaseProduct('suit'))
await Promise.all([first, second])
assert.equal(useUserStore().coins, 200)
})
test('failed mutation does not block the next mutation in queue', async () => {
const meOk = createMeFixture({ coins: 180 })
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
const { api } = await import('../src/api/client')
const { ApiClientError } = await import('../src/api/errors')
applyMeResponse(createMeFixture({ coins: 10 }))
let call = 0
globalThis.routeHandlers!.set('/store/purchases', () => {
call += 1
if (call === 1) {
return { status: 400, body: { error: 'سکه کافی نداری!' } }
}
return { status: 200, body: meOk }
})
await assert.rejects(runMeMutation(() => api.purchaseProduct('castle')), ApiClientError)
await runMeMutation(() => api.purchaseProduct('suit'))
assert.equal(useUserStore().coins, 180)
})
test('logout prevents queued mutation from applying or sending after session change', async () => {
const meStale = createMeFixture({ coins: 999 })
const meCurrent = createMeFixture({ coins: 40 })
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
const { invalidateSession } = await import('../src/api/authSession')
const { useUserStore } = await import('../src/stores/user')
const { api } = await import('../src/api/client')
const { StaleSessionError } = await import('../src/api/errors')
applyMeResponse(createMeFixture({ coins: 10 }))
globalThis.routeHandlers!.set('/store/purchases', () => ({
status: 200,
body: meStale,
delayMs: 40,
}))
const pending = runMeMutation(() => api.purchaseProduct('castle'))
invalidateSession()
applyMeResponse(meCurrent)
await assert.rejects(pending, StaleSessionError)
assert.equal(useUserStore().coins, 40)
})
declare global {
// eslint-disable-next-line no-var
var routeHandlers: Map<string, (init?: RequestInit) => import('./helpers/fetchMock').MockResponse | Promise<import('./helpers/fetchMock').MockResponse>>
}