Files
gold_land_user/test/catalog-api.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

198 lines
6.4 KiB
TypeScript

/**
* Stage 4 — store / games / daily challenge API wiring and stale-response policy.
*/
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, type MockResponse } from './helpers/fetchMock'
test.beforeEach(async () => {
installFetchMock()
await resetAppTestState()
setActivePinia(createPinia())
const { resetUserStores } = await import('../src/api/sync')
resetUserStores()
})
test('store catalog loads products from GET /store/products', async () => {
const products = [
{ id: 'castle', title: 'قلعه', price: 5000, category: 'special', emoji: '🏰' },
]
globalThis.routeHandlers!.set('/store/products', () => ({ status: 200, body: products }))
const { useStoreCatalogStore } = await import('../src/stores/storeCatalog')
const store = useStoreCatalogStore()
await store.loadProducts()
assert.equal(store.loadStatus, 'ready')
assert.equal(store.products.length, 1)
assert.equal(store.products[0]?.id, 'castle')
})
test('store GET error surfaces retryable error state', async () => {
globalThis.routeHandlers!.set('/store/products', () => ({
status: 500,
body: { error: 'خطای سرور' },
}))
const { useStoreCatalogStore } = await import('../src/stores/storeCatalog')
const store = useStoreCatalogStore()
await store.loadProducts()
assert.equal(store.loadStatus, 'error')
assert.ok(store.loadError)
})
test('successful purchase applies server me without optimistic coin change', async () => {
const meBefore = createMeFixture({ coins: 1000 })
const meAfter = createMeFixture({ coins: 500 })
meAfter.progress.ownedItems = ['castle']
const { applyMeResponse } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
const { useStoreCatalogStore } = await import('../src/stores/storeCatalog')
applyMeResponse(meBefore)
assert.equal(useUserStore().coins, 1000)
globalThis.routeHandlers!.set('/store/purchases', () => ({
status: 200,
body: meAfter,
delayMs: 15,
}))
const store = useStoreCatalogStore()
const purchase = store.purchaseProduct('castle')
assert.equal(useUserStore().coins, 1000)
await purchase
assert.equal(useUserStore().coins, 500)
assert.ok(useUserStore().coins !== 1000)
})
test('failed purchase does not change coins', async () => {
const me = createMeFixture({ coins: 200 })
const { applyMeResponse } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
const { useStoreCatalogStore } = await import('../src/stores/storeCatalog')
applyMeResponse(me)
globalThis.routeHandlers!.set('/store/purchases', () => ({
status: 400,
body: { error: 'سکه کافی نداری!' },
}))
const store = useStoreCatalogStore()
const { ApiClientError } = await import('../src/api/errors')
await assert.rejects(store.purchaseProduct('castle'), ApiClientError)
assert.equal(useUserStore().coins, 200)
})
test('stale store catalog GET after logout does not repopulate products', async () => {
const { invalidateSession } = await import('../src/api/authSession')
const { useStoreCatalogStore } = await import('../src/stores/storeCatalog')
const { StaleSessionError } = await import('../src/api/errors')
globalThis.routeHandlers!.set('/store/products', () => ({
status: 200,
body: [{ id: 'x', title: 'X', price: 1, category: 'boost', emoji: '⚡' }],
delayMs: 30,
}))
const store = useStoreCatalogStore()
const load = store.loadProducts()
invalidateSession()
await load
assert.equal(store.products.length, 0)
assert.equal(store.loadStatus, 'idle')
})
test('queued purchases apply in server order even when first HTTP response is slower', async () => {
const me1 = createMeFixture({ coins: 100 })
const me2 = createMeFixture({ coins: 250 })
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
const { useUserStore } = await import('../src/stores/user')
applyMeResponse(createMeFixture({ coins: 50 }))
let purchaseCall = 0
globalThis.routeHandlers!.set('/store/purchases', () => {
purchaseCall += 1
if (purchaseCall === 1) {
return { status: 200, body: me1, delayMs: 35 }
}
return { status: 200, body: me2 }
})
const { api } = await import('../src/api/client')
await Promise.all([
runMeMutation(() => api.purchaseProduct('castle')),
runMeMutation(() => api.purchaseProduct('suit')),
])
assert.equal(useUserStore().coins, 250)
})
test('games catalog load uses GET /games', async () => {
globalThis.routeHandlers!.set('/games', () => ({
status: 200,
body: [
{
id: 'gold-guess',
title: 'حدس',
description: 'd',
icon: '📈',
color: '#000',
route: '/games/gold-guess',
},
],
}))
const { useGamesStore } = await import('../src/stores/games')
const games = useGamesStore()
await games.loadCatalog()
assert.equal(games.catalogLoadStatus, 'ready')
assert.equal(games.catalog[0]?.id, 'gold-guess')
})
test('daily challenge complete returns server feedback only after POST', async () => {
const me = createMeFixture({ coins: 10 })
const { applyMeResponse } = await import('../src/api/sync')
applyMeResponse(me)
globalThis.routeHandlers!.set('/challenges/daily', () => ({
status: 200,
body: {
id: 'daily-challenge',
question: 'Q?',
coinReward: 50,
xpReward: 30,
completed: false,
options: [{ id: 'gold', label: 'طلا', icon: '🥇', color: '#f5b62d' }],
},
}))
globalThis.routeHandlers!.set('/challenges/daily/complete', () => ({
status: 200,
body: {
...me,
user: { ...me.user, coins: 60 },
feedback: 'بازخورد سرور',
alreadyCompleted: false,
},
}))
const { useDailyChallengeStore } = await import('../src/stores/dailyChallenge')
const { useUserStore } = await import('../src/stores/user')
const challenge = useDailyChallengeStore()
await challenge.loadChallenge()
const result = await challenge.completeOption('gold')
assert.equal(result.feedback, 'بازخورد سرور')
assert.equal(challenge.lastFeedback, 'بازخورد سرور')
assert.equal(useUserStore().coins, 60)
})
declare global {
// eslint-disable-next-line no-var
var routeHandlers: Map<string, (init?: RequestInit) => MockResponse | Promise<MockResponse>>
}