Files
ballet-server/miniprogram/tests/practice.test.cjs
T
yuxuanhui 6224ef5980 feat: add growth tracking page and related functionality
- Implemented a new Growth page to track practice time and trends.
- Added API integration for fetching review data.
- Created components for displaying practice statistics and trends.
- Updated navigation titles for the main index and growth pages.
- Removed unused styles from the index page.
- Introduced a Projects management page for adding and editing practice projects.
- Developed a Record form for logging practice sessions with validation.
- Added utility functions for date manipulation and duration formatting.
- Implemented error handling and session management in the practice service.
- Created unit tests for the practice service to ensure reliability.
2026-09-29 13:47:26 +08:00

85 lines
4.0 KiB
JavaScript

const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const ts = require('typescript')
// Exercise the exported client against the platform network/storage seam. No
// frontend E2E framework or production authentication bypass is introduced.
function client(network, options = {}) {
const storage = new Map()
let loginNumber = 0
const taro = {
login: async () => ({ code: `login-${++loginNumber}` }),
getStorageSync: key => storage.get(key),
setStorageSync: (key, value) => storage.set(key, value),
removeStorageSync: key => { if (options.storageRemovalFails) throw new Error('storage unavailable'); storage.delete(key) },
request: network,
}
const source = fs.readFileSync(path.join(__dirname, '../src/services/practice.ts'), 'utf8')
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true } }).outputText
const exports = {}
vm.runInNewContext(js, { exports, require: name => { assert.equal(name, '@tarojs/taro'); return taro }, __API_BASE_URL__: 'https://local-test.invalid' })
return exports
}
const ok = data => ({ statusCode: 200, data })
const session = token => ok({ token, expiresAt: '2099-01-01T00:00:00Z' })
const failure = (status, code) => ({ statusCode: status, data: { error: { code, message: code } } })
// Same public API calls as the pages: a renewed token cannot change a submitted
// form or create a fresh operation, including when platform storage removal fails.
test('expired sessions renew and retain the exact submission', async () => {
let exchange = 0
const payloads = []
const service = client(async request => {
if (request.url.endsWith('/session')) return session(`token-${++exchange}`)
payloads.push(request.data)
if (request.header.Authorization === 'Bearer token-1') return failure(401, 'unauthorized')
return ok({ id: 'record-1' })
}, { storageRemovalFails: true })
const input = { projectId: 'p', date: '2026-09-28', minutes: 90, note: '', requestId: 'same-submission-1' }
assert.equal((await service.api.saveRecord(input)).id, 'record-1')
assert.equal(exchange, 2)
assert.deepEqual(payloads, [input, input])
})
test('uncertain save stays recoverable and an explicit retry keeps its key', async () => {
let lost = true
const payloads = []
const service = client(async request => {
if (request.url.endsWith('/session')) return session('token')
payloads.push(request.data)
if (lost) { lost = false; throw new Error('response lost') }
return ok({ id: 'record-1' })
})
const pending = { input: { projectId: 'p', date: '2026-09-28', minutes: 90, note: 'keep this', requestId: service.submissionID() } }
service.retainPendingRecord(pending)
await assert.rejects(service.api.saveRecord(pending.input), error => error.status === 0)
const resumed = service.getPendingRecord()
assert.equal(resumed, pending)
assert.equal((await service.api.saveRecord(resumed.input)).id, 'record-1')
assert.equal(payloads[0].requestId, payloads[1].requestId)
service.clearPendingRecord(resumed)
assert.equal(service.getPendingRecord(), undefined)
assert.notEqual(service.submissionID(), pending.input.requestId)
})
test('failed loads stay errors and only confirmed writes notify visible pages', async () => {
let failWrite = true
const service = client(async request => {
if (request.url.endsWith('/session')) return session('token')
if (request.method === 'GET') return failure(503, 'service_unavailable')
if (failWrite) { failWrite = false; return failure(503, 'service_unavailable') }
return ok({ id: 'record-1' })
})
let changes = 0
const stop = service.onPracticeChange(() => { changes++ })
await assert.rejects(service.api.records(), error => error.status === 503)
await assert.rejects(service.api.saveRecord({}), error => error.status === 503)
assert.equal(changes, 0)
await service.api.saveRecord({})
assert.equal(changes, 1)
stop()
})