test(web): standardize frontend tests on Vitest (#6569)

* test(web): standardize frontend tests on Vitest

- configure Vitest, jsdom, and React Testing Library with shared test scripts.
- migrate existing node:test suites to the Vitest runner.
- rewrite JsonCodeEditor component tests with RTL and remove the direct happy-dom dependency.

* fix(ci): run frontend tests with Vitest

- invoke the configured Vitest script so browser test setup loads in CI.
- migrate remaining node:test suites to Vitest lifecycle APIs.

* test(web): use shared jsdom environment for component tests

- migrate usage cost and tool price tests to React Testing Library.
- remove duplicate happy-dom globals and rely on the configured Vitest setup.

* test(web): verify behavior with shared vitest setup

- replace Node test assertions with Vitest expect across frontend suites.
- migrate Keys component tests to React Testing Library interactions.
- centralize jsdom browser mocks for consistent component execution.

* fix(web): unblock frozen installs and Vitest CI

- sync dompurify 3.4.13 metadata into the Bun lockfile.
- replace the bun:test and happy-dom redemption harness with Vitest and RTL.
- preserve quota conversion, error feedback, and stale-response coverage in jsdom.
This commit is contained in:
QuentinHsu
2026-08-15 14:18:10 +08:00
committed by GitHub
parent 116255f076
commit e2c7aa7b10
37 changed files with 1569 additions and 2173 deletions
+57 -66
View File
@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import assert from 'node:assert/strict'
import { afterEach, describe, test } from 'node:test'
import { QueryClient } from '@tanstack/react-query'
import { afterEach, describe, expect, test } from 'vitest'
import { useAuthStore, type AuthBundle } from '../stores/auth-store'
import {
@@ -59,10 +57,10 @@ afterEach(() => {
describe('authentication session coordination', () => {
test('bootstrap distinguishes a completed anonymous check from an active session', async () => {
useAuthStore.getState().auth.reset('complete')
assert.deepEqual(await bootstrapAuthentication(), { kind: 'anonymous' })
expect(await bootstrapAuthentication()).toEqual({ kind: 'anonymous' })
useAuthStore.getState().auth.setBundle(bundle)
assert.deepEqual(await bootstrapAuthentication(), {
expect(await bootstrapAuthentication()).toEqual({
kind: 'authenticated',
bundle,
})
@@ -97,10 +95,10 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'authenticated')
assert.deepEqual(requestedSIDs, [bundle.session.sid, undefined])
assert.deepEqual(clears, [[false, 'idle']])
assert.deepEqual(accepted, [bundle])
expect(outcome.kind).toBe('authenticated')
expect(requestedSIDs).toEqual([bundle.session.sid, undefined])
expect(clears).toEqual([[false, 'idle']])
expect(accepted).toEqual([bundle])
})
test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => {
@@ -117,10 +115,10 @@ describe('authentication session coordination', () => {
wait: async () => undefined,
}
assert.deepEqual(await createRefreshRunner(runtime)(), {
expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'anonymous',
})
assert.deepEqual(clears, [[true, undefined]])
expect(clears).toEqual([[true, undefined]])
})
test('a temporary refresh failure remains retryable without clearing the session', async () => {
@@ -142,9 +140,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error')
assert.equal(clearCount, 0)
assert.equal(transientCount, 1)
expect(outcome.kind).toBe('transient_error')
expect(clearCount).toBe(0)
expect(transientCount).toBe(1)
})
test('a rate limited refresh remains retryable without clearing the session', async () => {
@@ -166,9 +164,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error')
assert.equal(clearCount, 0)
assert.equal(transientCount, 1)
expect(outcome.kind).toBe('transient_error')
expect(clearCount).toBe(0)
expect(transientCount).toBe(1)
})
test('an exhausted refresh race clears the unusable local session', async () => {
@@ -191,12 +189,12 @@ describe('authentication session coordination', () => {
},
}
assert.deepEqual(await createRefreshRunner(runtime)(), {
expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync',
code: 'AUTH_REFRESH_RACE',
})
assert.deepEqual(requestedDelays, [80, 200, 500])
assert.deepEqual(clears, [[false, undefined]])
expect(requestedDelays).toEqual([80, 200, 500])
expect(clears).toEqual([[false, undefined]])
})
test('an unexpected successful response is treated as out of sync', async () => {
@@ -213,11 +211,11 @@ describe('authentication session coordination', () => {
wait: async () => undefined,
}
assert.deepEqual(await createRefreshRunner(runtime)(), {
expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync',
code: 'AUTH_INVALID_REFRESH_RESPONSE',
})
assert.equal(cleared, true)
expect(cleared).toBe(true)
})
test('a refresh response cannot restore credentials after a newer auth operation', async () => {
@@ -241,8 +239,8 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error')
assert.equal(accepted, false)
expect(outcome.kind).toBe('transient_error')
expect(accepted).toBe(false)
})
test('explicit rotations update only the current session', () => {
@@ -254,41 +252,35 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, last_active_at: 200 },
})
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
assert.strictEqual(useAuthStore.getState().auth.user, bundle.user)
expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
expect(useAuthStore.getState().auth.user).toBe(bundle.user)
assert.throws(
() =>
applyAuthRotation({
access_token: 'non-bearer-token',
token_type: 'Custom',
access_expires_at: bundle.access_expires_at + 120,
session: bundle.session,
}),
/Invalid authentication rotation response/
)
assert.throws(
() =>
applyAuthRotation({
access_token: 'non-current-token',
token_type: 'Bearer',
access_expires_at: bundle.access_expires_at + 120,
session: { ...bundle.session, current: false },
}),
/Invalid authentication rotation response/
)
expect(() =>
applyAuthRotation({
access_token: 'non-bearer-token',
token_type: 'Custom',
access_expires_at: bundle.access_expires_at + 120,
session: bundle.session,
})
).toThrow(/Invalid authentication rotation response/)
expect(() =>
applyAuthRotation({
access_token: 'non-current-token',
token_type: 'Bearer',
access_expires_at: bundle.access_expires_at + 120,
session: { ...bundle.session, current: false },
})
).toThrow(/Invalid authentication rotation response/)
assert.throws(
() =>
applyAuthRotation({
access_token: 'wrong-session-token',
token_type: 'Bearer',
access_expires_at: bundle.access_expires_at + 120,
session: { ...bundle.session, sid: 'session-b' },
}),
/session mismatch/
)
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
expect(() =>
applyAuthRotation({
access_token: 'wrong-session-token',
token_type: 'Bearer',
access_expires_at: bundle.access_expires_at + 120,
session: { ...bundle.session, sid: 'session-b' },
})
).toThrow(/session mismatch/)
expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
})
test('sign-out clears user-scoped query, mutation, and authentication state', () => {
@@ -305,13 +297,13 @@ describe('authentication session coordination', () => {
clearAuthenticatedClientState(queryClient, false)
assert.equal(queryClient.getQueryCache().getAll().length, 0)
assert.equal(queryClient.getMutationCache().getAll().length, 0)
assert.equal(useAuthStore.getState().auth.user, null)
assert.equal(useAuthStore.getState().auth.accessToken, null)
assert.equal(useAuthStore.getState().auth.session, null)
assert.equal(useAuthStore.getState().auth.pending2FAFlowToken, null)
assert.equal(useAuthStore.getState().auth.bootstrapState, 'complete')
expect(queryClient.getQueryCache().getAll().length).toBe(0)
expect(queryClient.getMutationCache().getAll().length).toBe(0)
expect(useAuthStore.getState().auth.user).toBe(null)
expect(useAuthStore.getState().auth.accessToken).toBe(null)
expect(useAuthStore.getState().auth.session).toBe(null)
expect(useAuthStore.getState().auth.pending2FAFlowToken).toBe(null)
expect(useAuthStore.getState().auth.bootstrapState).toBe('complete')
const nextBundle: AuthBundle = {
...bundle,
@@ -320,8 +312,7 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, sid: 'session-b' },
}
useAuthStore.getState().auth.setBundle(nextBundle)
assert.equal(
queryClient.getQueryData(['account', bundle.user.id]),
expect(queryClient.getQueryData(['account', bundle.user.id])).toBe(
undefined
)
})
+11 -17
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import assert from 'node:assert/strict'
import { describe, test } from 'node:test'
import { describe, expect, test } from 'vitest'
import { resolveLegacyRoute } from './legacy-route'
@@ -43,17 +42,15 @@ describe('legacy frontend route migration', () => {
}
for (const [source, target] of Object.entries(routes)) {
assert.equal(resolveLegacyRoute(source), target)
expect(resolveLegacyRoute(source)).toBe(target)
}
})
test('preserves search and hash while applying route-specific behavior', () => {
assert.equal(
resolveLegacyRoute('/login?redirect=%2Fkeys#continue'),
expect(resolveLegacyRoute('/login?redirect=%2Fkeys#continue')).toBe(
'/sign-in?redirect=%2Fkeys#continue'
)
assert.equal(
resolveLegacyRoute('/console/topup?source=email#orders'),
expect(resolveLegacyRoute('/console/topup?source=email#orders')).toBe(
'/wallet?source=email#orders'
)
})
@@ -75,23 +72,20 @@ describe('legacy frontend route migration', () => {
}
for (const [tab, target] of Object.entries(settingsTabs)) {
assert.equal(
resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`),
`${target}?tab=${tab}&from=bookmark#form`
)
expect(
resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`)
).toBe(`${target}?tab=${tab}&from=bookmark#form`)
}
assert.equal(
resolveLegacyRoute('/console/setting?tab=unknown'),
expect(resolveLegacyRoute('/console/setting?tab=unknown')).toBe(
'/system-settings?tab=unknown'
)
})
test('safely redirects unknown console locations without touching new routes', () => {
assert.equal(
resolveLegacyRoute('/console/removed?page=2#old'),
expect(resolveLegacyRoute('/console/removed?page=2#old')).toBe(
'/dashboard?page=2#old'
)
assert.equal(resolveLegacyRoute('/dashboard'), null)
assert.equal(resolveLegacyRoute('/api/status'), null)
expect(resolveLegacyRoute('/dashboard')).toBe(null)
expect(resolveLegacyRoute('/api/status')).toBe(null)
})
})
+9 -11
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import assert from 'node:assert/strict'
import { describe, test } from 'node:test'
import { describe, expect, test } from 'vitest'
import { getServerErrorMessageKey } from './server-error-message'
@@ -25,8 +24,8 @@ describe('server error message mapping', () => {
test('maps the active-session limit to recovery instructions', () => {
const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' })
assert.match(message ?? '', /Sign out other sessions/)
assert.match(message ?? '', /reset your password/)
expect(message ?? '').toMatch(/Sign out other sessions/)
expect(message ?? '').toMatch(/reset your password/)
})
test('maps an Axios-shaped issuance limit to rolling-window guidance', () => {
@@ -34,8 +33,8 @@ describe('server error message mapping', () => {
response: { data: { code: 'AUTH_SESSION_ISSUANCE_LIMIT' } },
})
assert.match(message ?? '', /rolling window/)
assert.equal(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' }), null)
expect(message ?? '').toMatch(/rolling window/)
expect(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' })).toBe(null)
})
test('maps stable Telegram bind errors without exposing server text', () => {
@@ -55,16 +54,15 @@ describe('server error message mapping', () => {
}
for (const [code, message] of Object.entries(expected)) {
assert.equal(getServerErrorMessageKey({ code }), message)
expect(getServerErrorMessageKey({ code })).toBe(message)
}
assert.equal(
expect(
getServerErrorMessageKey({
response: {
data: { code: 'TELEGRAM_BIND_INTERNAL_ERROR', message: 'raw detail' },
},
}),
expected.TELEGRAM_BIND_INTERNAL_ERROR
)
})
).toBe(expected.TELEGRAM_BIND_INTERNAL_ERROR)
})
})