Commit e2c7aa7b by QuentinHsu Committed by GitHub

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.
parent 116255f0
......@@ -85,4 +85,4 @@ jobs:
run: bun run typecheck
- name: Test
run: bun test
run: bun run test
......@@ -11,6 +11,8 @@
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint -c .oxlintrc.json . --fix",
"preview": "rsbuild preview",
"test": "vitest run",
"test:watch": "vitest",
"format:check": "node scripts/format-with-protected-headers.mjs --check",
"format": "node scripts/format-with-protected-headers.mjs --write",
"copyright:check": "node scripts/add-copyright.mjs --check",
......@@ -85,17 +87,21 @@
"@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0",
"happy-dom": "^20.11.1",
"jsdom": "^29.1.1",
"knip": "^6.24.0",
"oxfmt": "^0.57.0",
"oxlint": "^1.72.0",
"shadcn": "^4.12.0"
"shadcn": "^4.12.0",
"vitest": "^4.1.10"
},
"overrides": {
"brace-expansion": "2.1.1",
......
......@@ -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 {
applyJsonSmartEnter,
......@@ -29,42 +28,42 @@ import {
describe('json code editor utils', () => {
test('treats empty drafts as valid editable JSON drafts', () => {
assert.deepEqual(getJsonValidationState(' \n'), {
expect(getJsonValidationState(' \n')).toEqual({
isValid: true,
messageKey: 'JSON',
})
})
test('reports invalid JSON without throwing away the draft', () => {
assert.deepEqual(getJsonValidationState('{"model": }'), {
expect(getJsonValidationState('{"model": }')).toEqual({
isValid: false,
messageKey: 'Invalid JSON',
})
})
test('formats valid JSON with stable two-space indentation', () => {
assert.deepEqual(formatJsonDraft('{"model":{"ratio":2}}'), {
expect(formatJsonDraft('{"model":{"ratio":2}}')).toEqual({
didFormat: true,
value: '{\n "model": {\n "ratio": 2\n }\n}',
})
})
test('keeps invalid JSON drafts unchanged when formatting is requested', () => {
assert.deepEqual(formatJsonDraft('{"model": }'), {
expect(formatJsonDraft('{"model": }')).toEqual({
didFormat: false,
value: '{"model": }',
})
})
test('derives the one-based cursor line and column from text offsets', () => {
assert.deepEqual(getCursorLocation('{\n "model": 1\n}', 5), {
expect(getCursorLocation('{\n "model": 1\n}', 5)).toEqual({
line: 2,
column: 4,
})
})
test('expands paired JSON brackets with a nested indentation line', () => {
assert.deepEqual(applyJsonSmartEnter('{}', 1, 1), {
expect(applyJsonSmartEnter('{}', 1, 1)).toEqual({
value: '{\n \n}',
selectionStart: 4,
selectionEnd: 4,
......@@ -90,11 +89,11 @@ describe('json code editor utils', () => {
source.scrollTop = 80
synchronizer.sync()
assert.equal(queuedFrames.length, 1)
expect(queuedFrames.length).toBe(1)
queuedFrames[0]()
assert.equal(contentLayer.style.transform, 'translate3d(-24px, -80px, 0)')
assert.equal(lineNumberLayer.style.transform, 'translate3d(0, -80px, 0)')
expect(contentLayer.style.transform).toBe('translate3d(-24px, -80px, 0)')
expect(lineNumberLayer.style.transform).toBe('translate3d(0, -80px, 0)')
})
})
......@@ -16,165 +16,98 @@ 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 { after, describe, test } from 'node:test'
import { Window } from 'happy-dom'
const domWindow = new Window()
const domGlobals = [
'window',
'document',
'navigator',
'HTMLElement',
'HTMLTextAreaElement',
'Node',
'Element',
'Event',
'CustomEvent',
'MutationObserver',
'requestAnimationFrame',
'cancelAnimationFrame',
'getComputedStyle',
] as const
for (const key of domGlobals) {
Object.defineProperty(globalThis, key, {
configurable: true,
value: domWindow[key],
})
}
const { act } = await import('react')
const { createRoot } = await import('react-dom/client')
const i18next = (await import('i18next')).default
const { initReactI18next } = await import('react-i18next')
await i18next.use(initReactI18next).init({
lng: 'en',
resources: {
en: {
translation: {
JSON: 'JSON',
'Invalid JSON': 'Invalid JSON',
'Copied to clipboard': 'Copied to clipboard',
'Failed to copy': 'Failed to copy',
'Format JSON': 'Format JSON',
},
},
},
})
const { JsonCodeEditor } = await import('../../json-code-editor')
const reactTestGlobals = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean
}
reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
type RenderedEditor = {
container: HTMLDivElement
root: ReturnType<typeof createRoot>
}
async function renderEditor(
props: React.ComponentProps<typeof JsonCodeEditor>
): Promise<RenderedEditor> {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () => {
root.render(<JsonCodeEditor {...props} />)
})
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, test, vi } from 'vitest'
return { container, root }
}
async function unmountEditor(rendered: RenderedEditor) {
await act(async () => rendered.root.unmount())
rendered.container.remove()
}
import { JsonCodeEditor } from '../../json-code-editor'
describe('JsonCodeEditor component', () => {
after(() => {
domWindow.close()
test('forwards form attributes and the textarea ref', () => {
const textareaRef = vi.fn()
const rendered = render(
<JsonCodeEditor
value='{"model":"gpt"}'
onChange={() => undefined}
id='json-input'
name='model_config'
placeholder='{"model":"gpt"}'
disabled
ariaLabel='Model configuration'
aria-describedby='model-help'
aria-invalid
data-form-root='settings-form'
textareaRef={textareaRef}
/>
)
const textarea = screen.getByRole('textbox', {
name: 'Model configuration',
})
test('forwards form attributes and lifecycle callbacks to the textarea', async () => {
const blurCalls: number[] = []
const refValues: Array<HTMLTextAreaElement | null> = []
const rendered = await renderEditor({
value: '{"model":"gpt"}',
onChange: () => undefined,
id: 'json-input',
name: 'model_config',
placeholder: '{"model":"gpt"}',
disabled: true,
'aria-describedby': 'model-help',
'aria-invalid': true,
'data-form-root': 'settings-form',
onBlur: () => blurCalls.push(1),
textareaRef: (element) => refValues.push(element),
})
const textarea = rendered.container.querySelector('textarea')
assert.ok(textarea)
assert.equal(textarea.id, 'json-input')
assert.equal(textarea.name, 'model_config')
assert.equal(textarea.placeholder, '{"model":"gpt"}')
assert.equal(textarea.disabled, true)
assert.equal(textarea.getAttribute('aria-describedby'), 'model-help')
assert.equal(textarea.getAttribute('aria-invalid'), 'true')
assert.equal(textarea.getAttribute('data-form-root'), 'settings-form')
await act(async () => textarea.dispatchEvent(new Event('blur')))
assert.deepEqual(blurCalls, [1])
assert.equal(refValues[0], textarea)
await unmountEditor(rendered)
assert.equal(refValues.at(-1), null)
expect(textarea).toHaveAttribute('id', 'json-input')
expect(textarea).toHaveAttribute('name', 'model_config')
expect(textarea).toHaveAttribute('placeholder', '{"model":"gpt"}')
expect(textarea).toBeDisabled()
expect(textarea).toHaveAttribute('aria-describedby', 'model-help')
expect(textarea).toHaveAttribute('aria-invalid', 'true')
expect(textarea).toHaveAttribute('data-form-root', 'settings-form')
expect(textareaRef).toHaveBeenCalledWith(textarea)
rendered.unmount()
expect(textareaRef).toHaveBeenLastCalledWith(null)
})
test('emits user edits and synchronizes a controlled value', async () => {
const changes: string[] = []
const rendered = await renderEditor({
value: '{"count":1}',
onChange: (value) => changes.push(value),
})
const textarea = rendered.container.querySelector('textarea')
test('calls onBlur when focus leaves the editor', () => {
const onBlur = vi.fn()
render(
<JsonCodeEditor
value='{}'
onChange={() => undefined}
onBlur={onBlur}
ariaLabel='Model configuration'
/>
)
fireEvent.blur(screen.getByRole('textbox', { name: 'Model configuration' }))
assert.ok(textarea)
await act(async () => {
textarea.value = '{"count":2}'
textarea.dispatchEvent(new Event('input', { bubbles: true }))
expect(onBlur).toHaveBeenCalledOnce()
})
assert.deepEqual(changes, ['{"count":2}'])
await act(async () => {
rendered.root.render(
test('emits user edits and synchronizes a controlled value', () => {
const onChange = vi.fn()
const rendered = render(
<JsonCodeEditor
value='{"count":3}'
onChange={(value) => changes.push(value)}
value='{"count":1}'
onChange={onChange}
ariaLabel='Model configuration'
/>
)
const textarea = screen.getByRole('textbox', {
name: 'Model configuration',
})
assert.equal(textarea.value, '{"count":3}')
await unmountEditor(rendered)
fireEvent.input(textarea, { target: { value: '{"count":2}' } })
expect(onChange).toHaveBeenCalledWith('{"count":2}')
rendered.rerender(
<JsonCodeEditor
value='{"count":3}'
onChange={onChange}
ariaLabel='Model configuration'
/>
)
expect(textarea).toHaveValue('{"count":3}')
})
test('formats valid JSON through the public toolbar action', async () => {
const changes: string[] = []
const rendered = await renderEditor({
value: '{"model":{"ratio":2}}',
onChange: (value) => changes.push(value),
})
const formatButton = [
...rendered.container.querySelectorAll('button'),
].find((button) => button.textContent?.includes('Format JSON'))
const user = userEvent.setup()
const onChange = vi.fn()
render(<JsonCodeEditor value='{"model":{"ratio":2}}' onChange={onChange} />)
assert.ok(formatButton)
await act(async () => formatButton.click())
assert.deepEqual(changes, ['{\n "model": {\n "ratio": 2\n }\n}'])
await user.click(screen.getByRole('button', { name: 'Format JSON' }))
await unmountEditor(rendered)
expect(onChange).toHaveBeenCalledWith(
'{\n "model": {\n "ratio": 2\n }\n}'
)
})
})
......@@ -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 {
modelGroupSelectorLayoutClasses,
......@@ -29,8 +28,8 @@ describe('model group selector layout', () => {
const groupScrollClasses =
modelGroupSelectorLayoutClasses.groupScroll.split(' ')
assert.ok(groupScrollClasses.includes('auto-rows-[2rem]'))
assert.ok(groupScrollClasses.includes('content-start'))
expect(groupScrollClasses.includes('auto-rows-[2rem]')).toBeTruthy()
expect(groupScrollClasses.includes('content-start')).toBeTruthy()
})
test('centers the selected group inside its own scroll container', () => {
......@@ -50,7 +49,7 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption, scrollContainer)
assert.deepEqual(scrollCalls, [{ top: 76, behavior: 'auto' }])
expect(scrollCalls).toEqual([{ top: 76, behavior: 'auto' }])
})
test('falls back to scrollIntoView when no group container is provided', () => {
......@@ -63,6 +62,6 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption)
assert.deepEqual(scrollCalls, [{ block: 'center', inline: 'nearest' }])
expect(scrollCalls).toEqual([{ block: 'center', inline: 'nearest' }])
})
})
......@@ -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 { handleDropdownMenuItemSelect } from './dropdown-menu-events'
......@@ -52,8 +51,8 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selected = true
})
assert.equal(selected, true)
assert.equal(event.baseUIHandlerPrevented, false)
expect(selected).toBe(true)
expect(event.baseUIHandlerPrevented).toBe(false)
})
test('keeps the Base UI menu open when onSelect prevents default', () => {
......@@ -63,7 +62,7 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selectEvent.preventDefault()
})
assert.equal(event.defaultPrevented, true)
assert.equal(event.baseUIHandlerPrevented, true)
expect(event.defaultPrevented).toBe(true)
expect(event.baseUIHandlerPrevented).toBe(true)
})
})
......@@ -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 type { RefreshOutcome } from '@/lib/api'
import type { AuthBundle } from '@/stores/auth-store'
......@@ -63,8 +62,8 @@ describe('logout coordination', () => {
},
})
assert.deepEqual(result, { success: false, message: 'not revoked' })
assert.equal(refreshCount, 0)
expect(result).toEqual({ success: false, message: 'not revoked' })
expect(refreshCount).toBe(0)
})
test('recovers a cookie mismatch and retries with the refreshed SID', async () => {
......@@ -83,8 +82,8 @@ describe('logout coordination', () => {
},
})
assert.deepEqual(result, { success: true, message: '' })
assert.deepEqual(requestedSIDs, ['session-a', 'session-b'])
expect(result).toEqual({ success: true, message: '' })
expect(requestedSIDs).toEqual(['session-a', 'session-b'])
})
test('treats a mismatch that refresh confirms anonymous as signed out', async () => {
......@@ -96,7 +95,7 @@ describe('logout coordination', () => {
refresh: async () => ({ kind: 'anonymous' }),
})
assert.deepEqual(result, { success: true, message: '' })
expect(result).toEqual({ success: true, message: '' })
})
test('preserves the active session when mismatch recovery is temporary', async () => {
......@@ -106,15 +105,14 @@ describe('logout coordination', () => {
error: new Error('offline'),
}
await assert.rejects(
await expect(
executeLogout({
getExpectedSID: () => 'session-a',
request: async () => {
throw originalError
},
refresh: async () => transient,
}),
(error) => error === originalError
)
})
).rejects.toBe(originalError)
})
})
......@@ -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 {
getOAuthSessionStorage,
......@@ -40,15 +39,14 @@ const bindState = 'bind-state'
describe('resolveOAuthCallbackMode', () => {
test('matching provider and state mark is treated as a bind flow', () => {
const storage = fakeStorage()
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), true)
expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(true)
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
}),
'bind'
)
})
).toBe('bind')
})
// Regression: a tab opened from an external link (Slack, e-mail, another
......@@ -58,75 +56,69 @@ describe('resolveOAuthCallbackMode', () => {
test('login redirect in a tab with a foreign opener stays a login flow', () => {
const storage = fakeStorage()
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
}),
'login'
)
})
).toBe('login')
})
test('bind marker for another provider does not hijack this callback', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'github', bindState)
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
}),
'login'
)
})
).toBe('login')
})
test('stale bind marker does not hijack a later callback', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', 'previous-state')
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
}),
'login'
)
})
).toBe('login')
})
test('bind marker without an opener falls back to login', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: null,
storage,
}),
'login'
)
})
).toBe('login')
})
test('closed opener falls back to login', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: { closed: true },
storage,
}),
'login'
)
})
).toBe('login')
})
test('missing storage degrades to login instead of throwing', () => {
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage: null,
}),
'login'
)
})
).toBe('login')
})
test('storage read failure degrades to login instead of throwing', () => {
......@@ -137,13 +129,12 @@ describe('resolveOAuthCallbackMode', () => {
setItem: () => undefined,
}
assert.equal(
expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
}),
'login'
)
})
).toBe('login')
})
})
......@@ -155,7 +146,7 @@ describe('OAuth bind popup storage', () => {
},
}
assert.equal(getOAuthSessionStorage(owner), null)
expect(getOAuthSessionStorage(owner)).toBe(null)
})
test('marking reports unavailable or unwritable storage', () => {
......@@ -166,9 +157,9 @@ describe('OAuth bind popup storage', () => {
},
}
assert.equal(markOAuthBindPopup(null, 'oidc', bindState), false)
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), false)
assert.equal(
expect(markOAuthBindPopup(null, 'oidc', bindState)).toBe(false)
expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(false)
expect(
markOAuthBindPopup(
{
getItem: () => null,
......@@ -176,8 +167,7 @@ describe('OAuth bind popup storage', () => {
},
'oidc',
bindState
),
false
)
).toBe(false)
})
})
......@@ -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 type { AuthUser } from '@/stores/auth-store'
......@@ -27,17 +26,15 @@ const origin = 'https://dashboard.example.com'
describe('authentication redirect validation', () => {
test('preserves safe internal paths, search parameters, and fragments', () => {
assert.equal(
sanitizeAuthRedirect('/console?tab=usage#recent', origin),
expect(sanitizeAuthRedirect('/console?tab=usage#recent', origin)).toBe(
'/console?tab=usage#recent'
)
assert.equal(
expect(
sanitizeAuthRedirect(
'https://dashboard.example.com/dashboard?tab=quota#daily',
origin
),
'/dashboard?tab=quota#daily'
)
).toBe('/dashboard?tab=quota#daily')
})
test('rejects external and ambiguously parsed redirect targets', () => {
......@@ -53,13 +50,13 @@ describe('authentication redirect validation', () => {
]
for (const target of unsafeTargets) {
assert.equal(sanitizeAuthRedirect(target, origin), null)
expect(sanitizeAuthRedirect(target, origin)).toBe(null)
}
})
test('rejects invalid or non-HTTP application origins', () => {
assert.equal(sanitizeAuthRedirect('/dashboard', 'not-an-origin'), null)
assert.equal(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app'), null)
expect(sanitizeAuthRedirect('/dashboard', 'not-an-origin')).toBe(null)
expect(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app')).toBe(null)
})
})
......@@ -67,31 +64,27 @@ describe('saved authentication language', () => {
const user: AuthUser = { id: 1, username: 'user', role: 1 }
test('prefers the explicit user language', () => {
assert.equal(
expect(
getSavedLanguage({
...user,
language: 'ja',
setting: { language: 'fr' },
}),
'ja'
)
})
).toBe('ja')
})
test('reads object and JSON string settings', () => {
assert.equal(
getSavedLanguage({ ...user, setting: { language: 'fr' } }),
expect(getSavedLanguage({ ...user, setting: { language: 'fr' } })).toBe(
'fr'
)
assert.equal(
getSavedLanguage({ ...user, setting: '{"language":"ru"}' }),
expect(getSavedLanguage({ ...user, setting: '{"language":"ru"}' })).toBe(
'ru'
)
})
test('ignores malformed and non-string setting languages', () => {
assert.equal(getSavedLanguage({ ...user, setting: '{' }), undefined)
assert.equal(
getSavedLanguage({ ...user, setting: { language: 123 } }),
expect(getSavedLanguage({ ...user, setting: '{' })).toBe(undefined)
expect(getSavedLanguage({ ...user, setting: { language: 123 } })).toBe(
undefined
)
})
......
......@@ -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 {
parseTelegramBindCallback,
......@@ -51,51 +50,48 @@ function fakeTimerRuntime() {
describe('OAuth bind popup lifecycle', () => {
test('parses Telegram success and stable error callbacks', () => {
assert.deepEqual(
expect(
parseTelegramBindCallback({
telegram_bind: 'success',
flow_token: 'flow-success',
}),
{
})
).toEqual({
kind: 'result',
flowToken: 'flow-success',
success: true,
}
)
assert.deepEqual(
})
expect(
parseTelegramBindCallback({
telegram_bind: 'error',
flow_token: 'flow-error',
error_code: 'TELEGRAM_BIND_ALREADY_BOUND',
}),
{
})
).toEqual({
kind: 'result',
flowToken: 'flow-error',
success: false,
code: 'TELEGRAM_BIND_ALREADY_BOUND',
}
)
})
})
test('rejects Telegram callbacks without a flow token and ignores descriptions', () => {
assert.deepEqual(parseTelegramBindCallback({ telegram_bind: 'error' }), {
expect(parseTelegramBindCallback({ telegram_bind: 'error' })).toEqual({
kind: 'invalid',
})
assert.deepEqual(
expect(
parseTelegramBindCallback({
telegram_bind: 'error',
flow_token: 'flow-error',
error_code: 'UNKNOWN_CODE',
error_description: 'untrusted message',
} as Parameters<typeof parseTelegramBindCallback>[0]),
{
} as Parameters<typeof parseTelegramBindCallback>[0])
).toEqual({
kind: 'result',
flowToken: 'flow-error',
success: false,
code: 'UNKNOWN_CODE',
}
)
assert.equal(parseTelegramBindCallback({}), null)
})
expect(parseTelegramBindCallback({})).toBe(null)
})
test('posts only complete Telegram bind results to an available opener', () => {
......@@ -112,11 +108,10 @@ describe('OAuth bind popup lifecycle', () => {
error_code: 'UNKNOWN_CODE',
})
assert.equal(
postTelegramBindResult(callback, opener, 'https://dashboard.example.com'),
true
)
assert.deepEqual(messages, [
expect(
postTelegramBindResult(callback, opener, 'https://dashboard.example.com')
).toBe(true)
expect(messages).toEqual([
{
message: {
type: 'telegram:binding:result',
......@@ -128,23 +123,17 @@ describe('OAuth bind popup lifecycle', () => {
},
])
assert.equal(
postTelegramBindResult(
{ kind: 'invalid' },
opener,
'https://example.com'
),
false
)
assert.equal(
expect(
postTelegramBindResult({ kind: 'invalid' }, opener, 'https://example.com')
).toBe(false)
expect(
postTelegramBindResult(
callback,
{ ...opener, closed: true },
'https://example.com'
),
false
)
assert.equal(messages.length, 1)
).toBe(false)
expect(messages.length).toBe(1)
})
test('waits 30 seconds for the opener response and can be cancelled', () => {
......@@ -158,11 +147,11 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime
)
assert.equal(timer.delay, 30_000)
expect(timer.delay).toBe(30_000)
cancel()
timer.fire()
assert.equal(timedOut, false)
assert.deepEqual(timer.cancelled, [timer.handle])
expect(timedOut).toBe(false)
expect(timer.cancelled).toEqual([timer.handle])
})
test('reports a closed popup once and clears its poller', () => {
......@@ -178,13 +167,13 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime
)
assert.equal(timer.delay, 500)
expect(timer.delay).toBe(500)
timer.fire()
assert.equal(closedCount, 0)
expect(closedCount).toBe(0)
popup.closed = true
timer.fire()
timer.fire()
assert.equal(closedCount, 1)
assert.deepEqual(timer.cancelled, [timer.handle])
expect(closedCount).toBe(1)
expect(timer.cancelled).toEqual([timer.handle])
})
})
......@@ -16,14 +16,13 @@ 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 { pickTelegramAuthorization } from './telegram-login'
describe('Telegram login authorization', () => {
test('keeps only fields signed by the Telegram login contract', () => {
assert.deepEqual(
expect(
pickTelegramAuthorization({
id: 12345,
first_name: 'Test',
......@@ -35,8 +34,8 @@ describe('Telegram login authorization', () => {
lang: 'en',
admin: true,
redirect: 'https://attacker.example',
}),
{
})
).toEqual({
id: 12345,
first_name: 'Test',
last_name: 'User',
......@@ -45,23 +44,17 @@ describe('Telegram login authorization', () => {
auth_date: 1_900_000_000,
hash: 'signed-hash',
lang: 'en',
}
)
})
})
test('rejects incomplete or structurally invalid callbacks', () => {
assert.equal(pickTelegramAuthorization(null), null)
assert.equal(
pickTelegramAuthorization({ auth_date: 1, hash: 'hash' }),
null
)
assert.equal(
pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' }),
null
)
assert.equal(
pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' }),
expect(pickTelegramAuthorization(null)).toBe(null)
expect(pickTelegramAuthorization({ auth_date: 1, hash: 'hash' })).toBe(null)
expect(pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' })).toBe(
null
)
expect(
pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' })
).toBe(null)
})
})
......@@ -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 {
CHANNEL_FIELD_UPDATE_DELAY_MS,
......@@ -31,7 +30,7 @@ function createFakeTimers() {
return {
timers: {
setTimeout: (callback: () => void, delay: number) => {
assert.equal(delay, CHANNEL_FIELD_UPDATE_DELAY_MS)
expect(delay).toBe(CHANNEL_FIELD_UPDATE_DELAY_MS)
const id = nextId++
pending.set(id, callback)
return id
......@@ -63,11 +62,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(1)
scheduler.schedule(2)
scheduler.schedule(3)
assert.deepEqual(updates, [])
assert.equal(fake.pendingCount, 1)
expect(updates).toEqual([])
expect(fake.pendingCount).toBe(1)
fake.fireAll()
assert.deepEqual(updates, [3])
expect(updates).toEqual([3])
})
test('flush commits the pending value immediately and cancels the timer', () => {
......@@ -80,11 +79,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(7)
scheduler.flush()
assert.deepEqual(updates, [7])
assert.equal(fake.pendingCount, 0)
expect(updates).toEqual([7])
expect(fake.pendingCount).toBe(0)
fake.fireAll()
assert.deepEqual(updates, [7])
expect(updates).toEqual([7])
})
test('flush without a pending value does nothing', () => {
......@@ -99,7 +98,7 @@ describe('channel field update scheduler', () => {
scheduler.schedule(5)
scheduler.flush()
scheduler.flush()
assert.deepEqual(updates, [5])
expect(updates).toEqual([5])
})
test('preserves a pending value of 0', () => {
......@@ -112,6 +111,6 @@ describe('channel field update scheduler', () => {
scheduler.schedule(0)
scheduler.flush()
assert.deepEqual(updates, [0])
expect(updates).toEqual([0])
})
})
......@@ -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 type { Channel } from '../../types'
import { getChannelTableRowId, type TagRow } from '../channel-utils'
......@@ -35,12 +34,8 @@ describe('channel table row identity', () => {
const beforeUpdate = [first, updated, third].map(getChannelTableRowId)
const afterUpdate = [updated, first, third].map(getChannelTableRowId)
assert.deepEqual(beforeUpdate, [
'channel:101',
'channel:202',
'channel:303',
])
assert.deepEqual(afterUpdate, ['channel:202', 'channel:101', 'channel:303'])
expect(beforeUpdate).toEqual(['channel:101', 'channel:202', 'channel:303'])
expect(afterUpdate).toEqual(['channel:202', 'channel:101', 'channel:303'])
})
test('uses separate namespaces for tag and channel rows', () => {
......@@ -50,7 +45,7 @@ describe('channel table row identity', () => {
children: [channel(202)],
} as TagRow
assert.equal(getChannelTableRowId(tagRow), 'tag:202')
assert.equal(getChannelTableRowId(channel(202)), 'channel:202')
expect(getChannelTableRowId(tagRow)).toBe('tag:202')
expect(getChannelTableRowId(channel(202))).toBe('channel:202')
})
})
......@@ -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 {
CHANNEL_TYPE_NEW_API,
......@@ -45,45 +44,40 @@ describe('New API channel', () => {
(item) => item.value === CHANNEL_TYPE_NEW_API
)
assert.deepEqual(option, {
expect(option).toEqual({
value: CHANNEL_TYPE_NEW_API,
label: 'New API',
})
assert.equal(
expect(
CHANNEL_TYPE_OPTIONS.findIndex(
(item) => item.value === CHANNEL_TYPE_NEW_API
) + 1,
CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58)
)
assert.equal(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API), true)
assert.equal(getChannelTypeIcon(CHANNEL_TYPE_NEW_API), 'NewAPI')
assert.equal(
getKeyPromptForType(CHANNEL_TYPE_NEW_API),
) + 1
).toBe(CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58))
expect(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API)).toBe(true)
expect(getChannelTypeIcon(CHANNEL_TYPE_NEW_API)).toBe('NewAPI')
expect(getKeyPromptForType(CHANNEL_TYPE_NEW_API)).toBe(
'Enter API key for this channel'
)
assert.equal(getChannelTypeConfig(CHANNEL_TYPE_NEW_API).icon, 'NewAPI')
expect(getChannelTypeConfig(CHANNEL_TYPE_NEW_API).icon).toBe('NewAPI')
})
test('requires a non-blank Base URL', () => {
const blankResult = channelFormSchema.safeParse(newAPIForm(' '))
assert.equal(blankResult.success, false)
expect(blankResult.success).toBe(false)
if (!blankResult.success) {
assert.equal(
expect(
blankResult.error.issues.some(
(issue) =>
issue.path[0] === 'base_url' &&
issue.message === 'Base URL is required for this channel type'
),
true
)
).toBe(true)
}
assert.equal(
channelFormSchema.safeParse(newAPIForm('https://new-api.example'))
.success,
true
)
expect(
channelFormSchema.safeParse(newAPIForm('https://new-api.example')).success
).toBe(true)
})
test('keeps Sub2API Base URL validation unchanged', () => {
......@@ -92,6 +86,6 @@ describe('New API channel', () => {
type: 59,
})
assert.equal(result.success, true)
expect(result.success).toBe(true)
})
})
......@@ -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 type { FlowUserFilterOption } from '../types'
import {
......@@ -46,89 +45,75 @@ const users: FlowUserFilterOption[] = [
describe('dashboard flow selection helpers', () => {
test('limits user chips to currently visible users', () => {
assert.deepEqual(
visibleFlowUsers(users, []).map((user) => user.value),
['user:1', 'user:2']
)
assert.deepEqual(
visibleFlowUsers(users, ['user:2']).map((user) => user.value),
['user:2']
)
expect(visibleFlowUsers(users, []).map((user) => user.value)).toEqual([
'user:1',
'user:2',
])
expect(
visibleFlowUsers(users, ['user:2']).map((user) => user.value)
).toEqual(['user:2'])
})
test('filters visible users without mutating the source options', () => {
const visible = visibleFlowUsers(users, ['user:1'])
assert.deepEqual(
visible.map((user) => user.value),
['user:1']
)
assert.deepEqual(
users.map((user) => user.value),
['user:1', 'user:2']
)
expect(visible.map((user) => user.value)).toEqual(['user:1'])
expect(users.map((user) => user.value)).toEqual(['user:1', 'user:2'])
})
test('formats compact selected counts for flow multiselect summaries', () => {
assert.equal(compactFlowSelectionLabel(0), '*')
assert.equal(compactFlowSelectionLabel(1), '1')
assert.equal(compactFlowSelectionLabel(23), '23')
expect(compactFlowSelectionLabel(0)).toBe('*')
expect(compactFlowSelectionLabel(1)).toBe('1')
expect(compactFlowSelectionLabel(23)).toBe('23')
})
test('prioritizes loading and error states before empty flow data', () => {
assert.equal(
expect(
flowDisplayState({
isLoading: true,
isError: true,
linkCount: 0,
themeReady: true,
}),
'loading'
)
assert.equal(
})
).toBe('loading')
expect(
flowDisplayState({
isLoading: false,
isError: true,
linkCount: 0,
themeReady: true,
}),
'error'
)
assert.equal(
})
).toBe('error')
expect(
flowDisplayState({
isLoading: false,
isError: false,
linkCount: 0,
themeReady: true,
}),
'empty'
)
assert.equal(
})
).toBe('empty')
expect(
flowDisplayState({
isLoading: false,
isError: false,
linkCount: 1,
themeReady: false,
}),
'loading'
)
})
).toBe('loading')
})
test('throws unsuccessful flow responses instead of treating them as empty data', () => {
assert.throws(
() =>
expect(() =>
requireSuccessfulFlowRows(
{ success: false, data: [], message: 'database unavailable' },
'Failed to load'
),
/database unavailable/
)
assert.deepEqual(
).toThrow(/database unavailable/)
expect(
requireSuccessfulFlowRows(
{ success: true, data: [{ user_id: 1, quota: 10 }] },
'Failed to load'
),
[{ user_id: 1, quota: 10 }]
)
).toEqual([{ user_id: 1, quota: 10 }])
})
})
......@@ -16,39 +16,9 @@ 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 { after, describe, test } from 'node:test'
import { Window } from 'happy-dom'
const domWindow = new Window()
const domGlobals = [
'window',
'document',
'navigator',
'HTMLElement',
'HTMLButtonElement',
'SVGElement',
'Node',
'Element',
'Event',
'CustomEvent',
'MutationObserver',
'ResizeObserver',
'requestAnimationFrame',
'cancelAnimationFrame',
'getComputedStyle',
] as const
for (const key of domGlobals) {
Object.defineProperty(globalThis, key, {
configurable: true,
value: domWindow[key],
})
}
import { render } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
const { act } = await import('react')
const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { TooltipProvider } = await import('@/components/ui/tooltip')
......@@ -70,11 +40,6 @@ await i18n.use(initReactI18next).init({
},
})
const reactTestGlobals = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean
}
reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
function CellHarness(props: {
group: string
ratio?: number | string
......@@ -96,17 +61,8 @@ function CellHarness(props: {
}
describe('API key group table cell', () => {
after(() => {
domWindow.close()
})
test('renders two unclipped rings and a localized Auto ratio when API data uses a nonlocalized string', async () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () =>
root.render(
test('renders an unclipped ring and a localized Auto ratio when API data uses a nonlocalized string', () => {
const { container } = render(
<CellHarness
group='auto'
ratio='自动'
......@@ -114,123 +70,84 @@ describe('API key group table cell', () => {
shouldReduceMotion={false}
/>
)
)
const badgeCell = container.querySelector<HTMLElement>(
'[data-api-key-group-cell="auto"]'
)
assert.ok(badgeCell)
assert.equal(badgeCell.classList.contains('overflow-visible'), true)
assert.equal(badgeCell.classList.contains('overflow-hidden'), false)
expect(badgeCell).toHaveClass('overflow-visible')
expect(badgeCell).not.toHaveClass('overflow-hidden')
const frames = container.querySelectorAll('[data-auto-group-frame]')
const movingRings = container.querySelectorAll(
'[data-auto-group-flow-border]'
)
assert.equal(frames.length, 2)
assert.equal(movingRings.length, 2)
expect(frames.length).toBe(1)
expect(movingRings.length).toBe(1)
for (const frame of frames) {
assert.equal(frame.classList.contains('relative'), true)
assert.equal(frame.classList.contains('overflow-visible'), true)
assert.equal(frame.classList.contains('rounded-4xl'), true)
assert.equal(frame.classList.contains('p-px'), true)
expect(frame).toHaveClass(
'relative',
'overflow-visible',
'rounded-4xl',
'p-px'
)
}
const ratio = container.querySelector<HTMLElement>(
'[data-auto-group-effect="ratio"]'
)
assert.ok(ratio)
assert.equal(ratio.textContent, 'Auto Ratio')
assert.equal(ratio.textContent?.includes('x'), false)
assert.equal(container.textContent?.includes('自动'), false)
assert.equal(container.textContent?.includes('Cross-group'), true)
expect(ratio).toHaveTextContent('Auto Ratio')
expect(ratio).not.toHaveTextContent('x')
expect(container).not.toHaveTextContent('自动')
expect(container).toHaveTextContent('Cross-group')
const crossGroupBadge = [
...container.querySelectorAll<HTMLElement>('[data-slot="status-badge"]'),
].find((badge) => badge.textContent === 'Cross-group')
assert.ok(crossGroupBadge)
assert.equal(crossGroupBadge.closest('[data-auto-group-frame]'), null)
await act(async () => root.unmount())
container.remove()
expect(crossGroupBadge).not.toBeUndefined()
expect(crossGroupBadge?.closest('[data-auto-group-frame]')).toBeNull()
})
test('keeps static Auto frames but omits both moving layers for reduced motion', async () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' ratio='Auto' shouldReduceMotion />)
test('keeps the static Auto ratio frame but omits its moving layer for reduced motion', () => {
const { container } = render(
<CellHarness group='auto' ratio='Auto' shouldReduceMotion />
)
assert.equal(
container.querySelectorAll('[data-auto-group-frame]').length,
2
)
assert.equal(
container.querySelectorAll('[data-auto-group-flow-border]').length,
0
)
await act(async () => root.unmount())
container.remove()
expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(1)
expect(
container.querySelectorAll('[data-auto-group-flow-border]').length
).toBe(0)
})
test('shows only the Auto badge when ratio data is unavailable', async () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' shouldReduceMotion={false} />)
test('shows only the cross-group badge when ratio data is unavailable', () => {
const { container } = render(
<CellHarness group='auto' shouldReduceMotion={false} />
)
assert.equal(
container.querySelectorAll('[data-auto-group-frame]').length,
1
)
assert.equal(
container.querySelectorAll('[data-auto-group-flow-border]').length,
1
)
assert.equal(
container.querySelector('[data-auto-group-effect="ratio"]'),
expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(0)
expect(
container.querySelectorAll('[data-auto-group-flow-border]').length
).toBe(0)
expect(container.querySelector('[data-auto-group-effect="ratio"]')).toBe(
null
)
assert.equal(container.textContent?.includes('Auto'), true)
assert.equal(container.textContent?.includes('Ratio'), false)
await act(async () => root.unmount())
container.remove()
expect(container).toHaveTextContent('Cross-group')
expect(container).not.toHaveTextContent('Auto')
expect(container).not.toHaveTextContent('Ratio')
})
test('narrows normal group ratios to numbers and never applies Auto rings', async () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () =>
root.render(
test('narrows normal group ratios to numbers and never applies Auto rings', () => {
const { container, rerender } = render(
<CellHarness group='vip' ratio='自动' shouldReduceMotion={false} />
)
)
assert.equal(container.textContent?.includes('vip'), true)
assert.equal(container.textContent?.includes('自动'), false)
assert.equal(container.querySelector('[data-auto-group-frame]'), null)
assert.equal(container.querySelector('[data-auto-group-flow-border]'), null)
await act(async () =>
root.render(
<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />
)
)
expect(container).toHaveTextContent('vip')
expect(container).not.toHaveTextContent('自动')
expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
expect(container.querySelector('[data-auto-group-flow-border]')).toBe(null)
assert.equal(container.textContent?.includes('3x'), true)
assert.equal(container.querySelector('[data-auto-group-frame]'), null)
rerender(<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />)
await act(async () => root.unmount())
container.remove()
expect(container).toHaveTextContent('3x')
expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
})
})
......@@ -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 { describe, test } from 'node:test'
import type { TFunction } from 'i18next'
import { describe, expect, test } from 'vitest'
import { apiKeySchema, type ApiKey } from '../../types'
import {
......@@ -60,16 +58,16 @@ describe('API key Auto group form mapping', () => {
const legacyApiKey: Record<string, unknown> = { ...baseApiKey }
delete legacyApiKey.auto_groups
assert.equal(apiKeySchema.parse(legacyApiKey).auto_groups, null)
expect(apiKeySchema.parse(legacyApiKey).auto_groups).toBe(null)
})
test('creates an Auto token that inherits the global order', () => {
const defaults = getApiKeyFormDefaultValues(true)
assert.equal(defaults.group, 'auto')
assert.equal(defaults.auto_groups_mode, 'inherit')
assert.deepEqual(defaults.auto_groups, [])
assert.deepEqual(transformFormDataToPayload(defaults).auto_groups, [])
expect(defaults.group).toBe('auto')
expect(defaults.auto_groups_mode).toBe('inherit')
expect(defaults.auto_groups).toEqual([])
expect(transformFormDataToPayload(defaults).auto_groups).toEqual([])
})
test('maps omitted, null, and empty snapshots to inheritance on edit', () => {
......@@ -88,8 +86,8 @@ describe('API key Auto group form mapping', () => {
2
)
assert.equal(defaults.auto_groups_mode, 'inherit')
assert.deepEqual(defaults.auto_groups, [])
expect(defaults.auto_groups_mode).toBe('inherit')
expect(defaults.auto_groups).toEqual([])
}
})
......@@ -103,8 +101,8 @@ describe('API key Auto group form mapping', () => {
2
)
assert.equal(defaults.auto_groups_mode, 'custom')
assert.deepEqual(defaults.auto_groups, ['vip', 'default'])
expect(defaults.auto_groups_mode).toBe('custom')
expect(defaults.auto_groups).toEqual(['vip', 'default'])
})
test('keeps a fully filtered snapshot custom and rejects it until resolved', () => {
......@@ -114,15 +112,14 @@ describe('API key Auto group form mapping', () => {
2
)
assert.equal(defaults.auto_groups_mode, 'custom')
assert.deepEqual(defaults.auto_groups, [])
expect(defaults.auto_groups_mode).toBe('custom')
expect(defaults.auto_groups).toEqual([])
const result = getApiKeyFormSchema(t, 2).safeParse(defaults)
assert.equal(result.success, false)
expect(result.success).toBe(false)
if (result.success) return
assert.deepEqual(result.error.issues[0]?.path, ['auto_groups'])
assert.equal(
result.error.issues[0]?.message,
expect(result.error.issues[0]?.path).toEqual(['auto_groups'])
expect(result.error.issues[0]?.message).toBe(
'Select at least one Auto group or restore global Auto.'
)
})
......@@ -134,7 +131,7 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'default'],
}
assert.deepEqual(transformFormDataToPayload(custom).auto_groups, [
expect(transformFormDataToPayload(custom).auto_groups).toEqual([
'vip',
'default',
])
......@@ -142,7 +139,7 @@ describe('API key Auto group form mapping', () => {
test('submits an empty array for inheritance and for non-Auto groups', () => {
const inherited = getApiKeyFormDefaultValues(true)
assert.deepEqual(transformFormDataToPayload(inherited).auto_groups, [])
expect(transformFormDataToPayload(inherited).auto_groups).toEqual([])
const nonAuto = {
...inherited,
......@@ -150,8 +147,8 @@ describe('API key Auto group form mapping', () => {
auto_groups_mode: 'custom' as const,
auto_groups: ['vip'],
}
assert.deepEqual(transformFormDataToPayload(nonAuto).auto_groups, [])
assert.equal(transformFormDataToPayload(nonAuto).cross_group_retry, false)
expect(transformFormDataToPayload(nonAuto).auto_groups).toEqual([])
expect(transformFormDataToPayload(nonAuto).cross_group_retry).toBe(false)
})
test('rejects snapshots over the configured limit', () => {
......@@ -162,13 +159,10 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['default', 'vip'],
})
assert.equal(result.success, false)
expect(result.success).toBe(false)
if (result.success) return
assert.equal(result.error.issues[0]?.path[0], 'auto_groups')
assert.equal(
result.error.issues[0]?.message,
'Select at most 1 Auto groups'
)
expect(result.error.issues[0]?.path[0]).toBe('auto_groups')
expect(result.error.issues[0]?.message).toBe('Select at most 1 Auto groups')
})
test('rejects duplicate custom groups', () => {
......@@ -179,10 +173,9 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'vip'],
})
assert.equal(result.success, false)
expect(result.success).toBe(false)
if (result.success) return
assert.equal(
result.error.issues[0]?.message,
expect(result.error.issues[0]?.message).toBe(
'Auto groups must not contain duplicates'
)
})
......
......@@ -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 type { ChatCompletionRequest } from '../types'
import { createStreamRequestController } from './use-stream-request'
......@@ -103,12 +102,12 @@ describe('latest-wins stream request coordination', () => {
const second = controller.send(payload, noopCallbacks)
firstHeaders.resolve({ Authorization: 'Bearer stale' })
await first
assert.equal(sources.length, 0)
expect(sources.length).toBe(0)
secondHeaders.resolve({ Authorization: 'Bearer current' })
await second
assert.equal(sources.length, 1)
assert.equal(sources[0]?.streamed, true)
expect(sources.length).toBe(1)
expect(sources[0]?.streamed).toBe(true)
})
test('stop cancels a request that is still waiting for headers', async () => {
......@@ -128,7 +127,7 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' })
await request
assert.equal(sourceCount, 0)
expect(sourceCount).toBe(0)
})
test('dispose cancels a pending header request without a state update', async () => {
......@@ -149,8 +148,8 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' })
await request
assert.equal(sourceCount, 0)
assert.deepEqual(streamingStates, [false])
expect(sourceCount).toBe(0)
expect(streamingStates).toEqual([false])
})
test('closes the previous source and ignores all of its later events', async () => {
......@@ -182,7 +181,7 @@ describe('latest-wins stream request coordination', () => {
await controller.send(payload, callbacks)
const second = controller.send(payload, callbacks)
assert.equal(sources[0]?.closed, true)
expect(sources[0]?.closed).toBe(true)
sources[0]?.emit(
'message',
JSON.stringify({ choices: [{ delta: { content: 'stale' } }] })
......@@ -195,6 +194,6 @@ describe('latest-wins stream request coordination', () => {
JSON.stringify({ choices: [{ delta: { content: 'current' } }] })
)
assert.deepEqual(updates, ['current'])
expect(updates).toEqual(['current'])
})
})
import type { TFunction } from 'i18next'
/*
Copyright (C) 2023-2026 QuantumNous
......@@ -16,10 +17,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 type { TFunction } from 'i18next'
import { describe, expect, test } from 'vitest'
import { loginMethodLabel, sessionDevice } from '../login-session-utils'
......@@ -27,14 +25,10 @@ const translate = ((key: string) => key) as TFunction
describe('login session presentation', () => {
test('labels built-in and provider OAuth login methods', () => {
assert.equal(loginMethodLabel('password', translate), 'Password')
assert.equal(
loginMethodLabel('2fa', translate),
'Two-factor Authentication'
)
assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
assert.equal(
loginMethodLabel('oauth:custom-provider', translate),
expect(loginMethodLabel('password', translate)).toBe('Password')
expect(loginMethodLabel('2fa', translate)).toBe('Two-factor Authentication')
expect(loginMethodLabel('oauth:github', translate)).toBe('OAuth · GitHub')
expect(loginMethodLabel('oauth:custom-provider', translate)).toBe(
'OAuth · custom-provider'
)
})
......@@ -43,8 +37,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
assert.equal(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
'Safari · iOS'
)
})
......@@ -53,8 +46,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
assert.equal(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
'Safari · iOS'
)
})
......@@ -63,8 +55,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
assert.equal(
sessionDevice(userAgent, 'Unknown device', 'Browser', 10),
expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 10)).toBe(
'Chrome · Windows'
)
})
......@@ -73,8 +64,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'
assert.equal(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
'Chrome · Android'
)
})
......@@ -83,15 +73,13 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
assert.equal(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
'Safari · macOS'
)
})
test('falls back to the unknown-device label for an empty user agent', () => {
assert.equal(
sessionDevice('', 'Unknown device', 'Browser'),
expect(sessionDevice('', 'Unknown device', 'Browser')).toBe(
'Unknown device'
)
})
......
......@@ -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 { positiveIntegerSchema } from '../../utils/numeric-field'
......@@ -26,15 +25,15 @@ const schema = positiveIntegerSchema(t('Enter a positive integer'))
describe('per-token Auto group limit validation', () => {
test('accepts any positive integer without a product upper bound', () => {
assert.equal(schema.safeParse(1000).success, true)
expect(schema.safeParse(1000).success).toBe(true)
})
test('rejects zero, negative, and fractional limits', () => {
for (const maxTokenAutoGroups of [0, -1, 1.5]) {
const result = schema.safeParse(maxTokenAutoGroups)
assert.equal(result.success, false)
expect(result.success).toBe(false)
if (result.success) continue
assert.equal(result.error.issues[0]?.message, 'Enter a positive integer')
expect(result.error.issues[0]?.message).toBe('Enter a positive integer')
}
})
})
......@@ -16,128 +16,49 @@ 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 { after, describe, test } from 'node:test'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import i18next from 'i18next'
import { beforeAll, describe, expect, test } from 'vitest'
import { Window } from 'happy-dom'
import { ToolPriceSettings } from '../tool-price-settings'
const domWindow = new Window()
const domGlobals = [
'window',
'document',
'navigator',
'HTMLElement',
'HTMLInputElement',
'SVGElement',
'Node',
'Element',
'Event',
'CustomEvent',
'MutationObserver',
'requestAnimationFrame',
'cancelAnimationFrame',
'getComputedStyle',
] as const
for (const key of domGlobals) {
Object.defineProperty(globalThis, key, {
configurable: true,
value: domWindow[key],
})
}
const { act } = await import('react')
const { createRoot } = await import('react-dom/client')
const { QueryClient, QueryClientProvider } =
await import('@tanstack/react-query')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { ToolPriceSettings } = await import('../tool-price-settings')
const i18n = createInstance()
await i18n.use(initReactI18next).init({
lng: 'en',
resources: {
en: {
translation: {
describe('tool price validation', () => {
beforeAll(() => {
i18next.addResourceBundle('en', 'translation', {
'Price ($/1K calls)': 'Price ($/1K calls)',
'Please enter a valid number': 'Please enter a valid number',
'Tool identifier': 'Tool identifier',
},
},
},
})
const reactTestGlobals = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean
}
reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
function changeInputValue(input: HTMLInputElement, value: string) {
const valueSetter = Object.getOwnPropertyDescriptor(
domWindow.HTMLInputElement.prototype,
'value'
)?.set
assert.ok(valueSetter)
valueSetter.call(input, value)
input.dispatchEvent(
new domWindow.Event('input', { bubbles: true }) as unknown as Event
)
}
describe('tool price validation', () => {
after(() => {
domWindow.close()
})
})
test('blocks an empty price without converting it to an explicit zero', async () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
test('blocks an empty price without converting it to an explicit zero', () => {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
})
await act(async () => {
root.render(
render(
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
<ToolPriceSettings defaultValue='{"web_search":10}' />
</I18nextProvider>
</QueryClientProvider>
)
})
const priceInput = container.querySelector<HTMLInputElement>(
'input[aria-label="Price ($/1K calls): web_search"]'
)
assert.ok(priceInput)
await act(async () => {
changeInputValue(priceInput, '')
const priceInput = screen.getByRole('spinbutton', {
name: 'Price ($/1K calls): web_search',
})
const saveButton = screen.getByRole('button', { name: 'Save tool prices' })
assert.equal(priceInput.getAttribute('aria-invalid'), 'true')
assert.equal(
priceInput.closest('[data-slot="field"]')?.querySelector('[role="alert"]')
?.textContent,
'Please enter a valid number'
)
const saveButton = [...container.querySelectorAll('button')].find(
(button) => button.textContent === 'Save tool prices'
)
assert.ok(saveButton)
assert.equal(saveButton.disabled, true)
fireEvent.change(priceInput, { target: { value: '' } })
await act(async () => {
changeInputValue(priceInput, '0')
})
expect(priceInput).toHaveAttribute('aria-invalid', 'true')
expect(screen.getByText('Please enter a valid number')).toBeInTheDocument()
expect(saveButton).toBeDisabled()
fireEvent.change(priceInput, { target: { value: '0' } })
assert.equal(priceInput.getAttribute('aria-invalid'), 'false')
assert.equal(saveButton.disabled, false)
expect(priceInput).toHaveAttribute('aria-invalid', 'false')
expect(saveButton).toBeEnabled()
await act(async () => root.unmount())
container.remove()
queryClient.clear()
})
})
......@@ -16,88 +16,19 @@ 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 { after, describe, test } from 'node:test'
import { Window } from 'happy-dom'
import { render, screen } from '@testing-library/react'
import i18next from 'i18next'
import type React from 'react'
import { beforeAll, describe, expect, test } from 'vitest'
const domWindow = new Window()
const domGlobals = [
'window',
'document',
'navigator',
'HTMLElement',
'SVGElement',
'Node',
'Element',
'Event',
'CustomEvent',
'MutationObserver',
'requestAnimationFrame',
'cancelAnimationFrame',
'getComputedStyle',
] as const
for (const key of domGlobals) {
Object.defineProperty(globalThis, key, {
configurable: true,
value: domWindow[key],
})
}
const { act } = await import('react')
const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const i18n = createInstance()
await i18n.use(initReactI18next).init({
lng: 'en',
resources: {
en: {
translation: {
Subscription: 'Subscription',
'Deducted by subscription': 'Deducted by subscription',
'Includes tool-call surcharge': 'Includes tool-call surcharge',
},
},
},
})
const { LogCostDisplay } = await import('../log-cost-display')
const { formatLogQuota } = await import('@/lib/format')
const reactTestGlobals = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean
}
reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
import { formatLogQuota } from '@/lib/format'
type RenderedCost = {
container: HTMLDivElement
root: ReturnType<typeof createRoot>
}
import { LogCostDisplay } from '../log-cost-display'
async function renderCost(
function renderCost(
props: React.ComponentProps<typeof LogCostDisplay>
): Promise<RenderedCost> {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () => {
root.render(
<I18nextProvider i18n={i18n}>
<LogCostDisplay {...props} />
</I18nextProvider>
)
})
return { container, root }
}
async function unmountCost(rendered: RenderedCost) {
await act(async () => rendered.root.unmount())
rendered.container.remove()
): ReturnType<typeof render> {
return render(<LogCostDisplay {...props} />)
}
function normalizedText(value: string | null): string {
......@@ -105,39 +36,36 @@ function normalizedText(value: string | null): string {
}
describe('log cost display', () => {
after(() => {
domWindow.close()
beforeAll(() => {
i18next.addResourceBundle('en', 'translation', {
Subscription: 'Subscription',
'Deducted by subscription': 'Deducted by subscription',
'Includes tool-call surcharge': 'Includes tool-call surcharge',
})
})
test('keeps the regular cost visible and adds an accessible surcharge marker', async () => {
const rendered = await renderCost({
test('keeps the regular cost visible and adds an accessible surcharge marker', () => {
const rendered = renderCost({
quota: 12500,
other: {
tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }],
},
})
assert.equal(
expect(
normalizedText(rendered.container.textContent).includes(
normalizedText(formatLogQuota(12500))
),
true
)
const marker = rendered.container.querySelector(
'[data-tool-surcharge-indicator="true"]'
)
assert.ok(marker)
assert.equal(
marker.getAttribute('aria-label'),
'Includes tool-call surcharge'
)
assert.equal(marker.getAttribute('tabindex'), '0')
await unmountCost(rendered)
).toBe(true)
const marker = screen.getByRole('img', {
name: 'Includes tool-call surcharge',
})
expect(marker).toHaveAttribute('data-tool-surcharge-indicator', 'true')
expect(marker).toHaveAttribute('tabindex', '0')
})
test('preserves the subscription badge and adds the same legacy surcharge marker', async () => {
const rendered = await renderCost({
test('preserves the subscription badge and adds the same legacy surcharge marker', () => {
renderCost({
quota: 5000,
other: {
billing_source: 'subscription',
......@@ -147,11 +75,9 @@ describe('log cost display', () => {
},
})
assert.equal(rendered.container.textContent?.includes('Subscription'), true)
assert.ok(
rendered.container.querySelector('[data-tool-surcharge-indicator="true"]')
)
await unmountCost(rendered)
expect(screen.getByText('Subscription')).toBeInTheDocument()
expect(
screen.getByRole('img', { name: 'Includes tool-call surcharge' })
).toHaveAttribute('data-tool-surcharge-indicator', 'true')
})
})
......@@ -16,20 +16,18 @@ 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 type { LogOtherData } from '../../types'
import { hasToolSurcharge } from '../format'
describe('tool surcharge detection', () => {
test('shows the marker for a charged structured tool surcharge', () => {
assert.equal(
expect(
hasToolSurcharge({
tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }],
}),
true
)
})
).toBe(true)
})
const legacyCases: Array<{
......@@ -63,7 +61,7 @@ describe('tool surcharge detection', () => {
for (const scenario of legacyCases) {
test(`keeps the marker visible for legacy ${scenario.name} charges`, () => {
assert.equal(hasToolSurcharge(scenario.other), true)
expect(hasToolSurcharge(scenario.other)).toBe(true)
})
}
......@@ -93,7 +91,7 @@ describe('tool surcharge detection', () => {
]
for (const other of invalidCases) {
assert.equal(hasToolSurcharge(other), false)
expect(hasToolSurcharge(other)).toBe(false)
}
})
})
......@@ -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 { PAYMENT_TYPES } from '../constants'
import { requestPaymentAmount } from './use-payment'
......@@ -44,7 +43,7 @@ describe('payment amount routing', () => {
},
})
assert.equal(amount, 18.75)
assert.deepEqual(calls, ['waffo:120'])
expect(amount).toBe(18.75)
expect(calls).toEqual(['waffo:120'])
})
})
......@@ -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 { PAYMENT_TYPES } from '../constants'
import {
......@@ -29,11 +28,11 @@ import {
describe('payment type classification', () => {
test('keeps Waffo and Waffo Pancake on their dedicated flows', () => {
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO), true)
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE), false)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE), true)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO), false)
assert.equal(isStripePayment(PAYMENT_TYPES.STRIPE), true)
expect(isWaffoPayment(PAYMENT_TYPES.WAFFO)).toBe(true)
expect(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(false)
expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(true)
expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO)).toBe(false)
expect(isStripePayment(PAYMENT_TYPES.STRIPE)).toBe(true)
})
})
......@@ -60,8 +59,8 @@ describe('payment dispatch', () => {
}
)
assert.equal(success, true)
assert.deepEqual(calls, ['waffo:120:3'])
expect(success).toBe(true)
expect(calls).toEqual(['waffo:120:3'])
})
test('does not create a Waffo order without a selected method index', async () => {
......@@ -80,7 +79,7 @@ describe('payment dispatch', () => {
}
)
assert.equal(success, false)
assert.equal(called, false)
expect(success).toBe(false)
expect(called).toBe(false)
})
})
......@@ -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(
() =>
expect(() =>
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(
() =>
})
).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 },
}),
/Invalid authentication rotation response/
)
})
).toThrow(/Invalid authentication rotation response/)
assert.throws(
() =>
expect(() =>
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')
})
).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
)
})
......
......@@ -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)
})
})
......@@ -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)
})
})
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import { afterEach, beforeAll } from 'vitest'
beforeAll(async () => {
await i18next.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
resources: {
en: {
translation: {},
},
},
})
})
afterEach(() => {
cleanup()
})
Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: (query: string): MediaQueryList => ({
matches: false,
media: query,
onchange: null,
addListener: () => undefined,
removeListener: () => undefined,
addEventListener: () => undefined,
removeEventListener: () => undefined,
dispatchEvent: () => false,
}),
})
window.requestAnimationFrame = (callback: FrameRequestCallback) =>
window.setTimeout(() => callback(performance.now()), 0)
window.cancelAnimationFrame = (handle: number) => window.clearTimeout(handle)
class ResizeObserverMock {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
Object.defineProperty(globalThis, 'ResizeObserver', {
configurable: true,
value: ResizeObserverMock,
})
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
configurable: true,
value: () => undefined,
})
......@@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["rsbuild.config.ts"]
"include": ["rsbuild.config.ts", "vitest.config.ts"]
}
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test-setup.ts'],
clearMocks: true,
restoreMocks: true,
include: ['src/**/*.{test,spec}.{ts,tsx}'],
},
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment