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: ...@@ -85,4 +85,4 @@ jobs:
run: bun run typecheck run: bun run typecheck
- name: Test - name: Test
run: bun test run: bun run test
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
"lint": "oxlint -c .oxlintrc.json .", "lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint -c .oxlintrc.json . --fix", "lint:fix": "oxlint -c .oxlintrc.json . --fix",
"preview": "rsbuild preview", "preview": "rsbuild preview",
"test": "vitest run",
"test:watch": "vitest",
"format:check": "node scripts/format-with-protected-headers.mjs --check", "format:check": "node scripts/format-with-protected-headers.mjs --check",
"format": "node scripts/format-with-protected-headers.mjs --write", "format": "node scripts/format-with-protected-headers.mjs --write",
"copyright:check": "node scripts/add-copyright.mjs --check", "copyright:check": "node scripts/add-copyright.mjs --check",
...@@ -85,17 +87,21 @@ ...@@ -85,17 +87,21 @@
"@tanstack/react-query-devtools": "^5.101.2", "@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19", "@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/node": "^26.1.0",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3", "@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1", "@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"happy-dom": "^20.11.1", "jsdom": "^29.1.1",
"knip": "^6.24.0", "knip": "^6.24.0",
"oxfmt": "^0.57.0", "oxfmt": "^0.57.0",
"oxlint": "^1.72.0", "oxlint": "^1.72.0",
"shadcn": "^4.12.0" "shadcn": "^4.12.0",
"vitest": "^4.1.10"
}, },
"overrides": { "overrides": {
"brace-expansion": "2.1.1", "brace-expansion": "2.1.1",
......
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
applyJsonSmartEnter, applyJsonSmartEnter,
...@@ -29,42 +28,42 @@ import { ...@@ -29,42 +28,42 @@ import {
describe('json code editor utils', () => { describe('json code editor utils', () => {
test('treats empty drafts as valid editable JSON drafts', () => { test('treats empty drafts as valid editable JSON drafts', () => {
assert.deepEqual(getJsonValidationState(' \n'), { expect(getJsonValidationState(' \n')).toEqual({
isValid: true, isValid: true,
messageKey: 'JSON', messageKey: 'JSON',
}) })
}) })
test('reports invalid JSON without throwing away the draft', () => { test('reports invalid JSON without throwing away the draft', () => {
assert.deepEqual(getJsonValidationState('{"model": }'), { expect(getJsonValidationState('{"model": }')).toEqual({
isValid: false, isValid: false,
messageKey: 'Invalid JSON', messageKey: 'Invalid JSON',
}) })
}) })
test('formats valid JSON with stable two-space indentation', () => { test('formats valid JSON with stable two-space indentation', () => {
assert.deepEqual(formatJsonDraft('{"model":{"ratio":2}}'), { expect(formatJsonDraft('{"model":{"ratio":2}}')).toEqual({
didFormat: true, didFormat: true,
value: '{\n "model": {\n "ratio": 2\n }\n}', value: '{\n "model": {\n "ratio": 2\n }\n}',
}) })
}) })
test('keeps invalid JSON drafts unchanged when formatting is requested', () => { test('keeps invalid JSON drafts unchanged when formatting is requested', () => {
assert.deepEqual(formatJsonDraft('{"model": }'), { expect(formatJsonDraft('{"model": }')).toEqual({
didFormat: false, didFormat: false,
value: '{"model": }', value: '{"model": }',
}) })
}) })
test('derives the one-based cursor line and column from text offsets', () => { 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, line: 2,
column: 4, column: 4,
}) })
}) })
test('expands paired JSON brackets with a nested indentation line', () => { test('expands paired JSON brackets with a nested indentation line', () => {
assert.deepEqual(applyJsonSmartEnter('{}', 1, 1), { expect(applyJsonSmartEnter('{}', 1, 1)).toEqual({
value: '{\n \n}', value: '{\n \n}',
selectionStart: 4, selectionStart: 4,
selectionEnd: 4, selectionEnd: 4,
...@@ -90,11 +89,11 @@ describe('json code editor utils', () => { ...@@ -90,11 +89,11 @@ describe('json code editor utils', () => {
source.scrollTop = 80 source.scrollTop = 80
synchronizer.sync() synchronizer.sync()
assert.equal(queuedFrames.length, 1) expect(queuedFrames.length).toBe(1)
queuedFrames[0]() queuedFrames[0]()
assert.equal(contentLayer.style.transform, 'translate3d(-24px, -80px, 0)') expect(contentLayer.style.transform).toBe('translate3d(-24px, -80px, 0)')
assert.equal(lineNumberLayer.style.transform, 'translate3d(0, -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/>. ...@@ -16,165 +16,98 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { fireEvent, render, screen } from '@testing-library/react'
import { after, describe, test } from 'node:test' import userEvent from '@testing-library/user-event'
import { describe, expect, test, vi } from 'vitest'
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} />)
})
return { container, root }
}
async function unmountEditor(rendered: RenderedEditor) { import { JsonCodeEditor } from '../../json-code-editor'
await act(async () => rendered.root.unmount())
rendered.container.remove()
}
describe('JsonCodeEditor component', () => { describe('JsonCodeEditor component', () => {
after(() => { test('forwards form attributes and the textarea ref', () => {
domWindow.close() const textareaRef = vi.fn()
}) const rendered = render(
<JsonCodeEditor
test('forwards form attributes and lifecycle callbacks to the textarea', async () => { value='{"model":"gpt"}'
const blurCalls: number[] = [] onChange={() => undefined}
const refValues: Array<HTMLTextAreaElement | null> = [] id='json-input'
const rendered = await renderEditor({ name='model_config'
value: '{"model":"gpt"}', placeholder='{"model":"gpt"}'
onChange: () => undefined, disabled
id: 'json-input', ariaLabel='Model configuration'
name: 'model_config', aria-describedby='model-help'
placeholder: '{"model":"gpt"}', aria-invalid
disabled: true, data-form-root='settings-form'
'aria-describedby': 'model-help', textareaRef={textareaRef}
'aria-invalid': true, />
'data-form-root': 'settings-form', )
onBlur: () => blurCalls.push(1), const textarea = screen.getByRole('textbox', {
textareaRef: (element) => refValues.push(element), name: 'Model configuration',
}) })
const textarea = rendered.container.querySelector('textarea')
expect(textarea).toHaveAttribute('id', 'json-input')
assert.ok(textarea) expect(textarea).toHaveAttribute('name', 'model_config')
assert.equal(textarea.id, 'json-input') expect(textarea).toHaveAttribute('placeholder', '{"model":"gpt"}')
assert.equal(textarea.name, 'model_config') expect(textarea).toBeDisabled()
assert.equal(textarea.placeholder, '{"model":"gpt"}') expect(textarea).toHaveAttribute('aria-describedby', 'model-help')
assert.equal(textarea.disabled, true) expect(textarea).toHaveAttribute('aria-invalid', 'true')
assert.equal(textarea.getAttribute('aria-describedby'), 'model-help') expect(textarea).toHaveAttribute('data-form-root', 'settings-form')
assert.equal(textarea.getAttribute('aria-invalid'), 'true') expect(textareaRef).toHaveBeenCalledWith(textarea)
assert.equal(textarea.getAttribute('data-form-root'), 'settings-form')
rendered.unmount()
await act(async () => textarea.dispatchEvent(new Event('blur'))) expect(textareaRef).toHaveBeenLastCalledWith(null)
assert.deepEqual(blurCalls, [1])
assert.equal(refValues[0], textarea)
await unmountEditor(rendered)
assert.equal(refValues.at(-1), null)
}) })
test('emits user edits and synchronizes a controlled value', async () => { test('calls onBlur when focus leaves the editor', () => {
const changes: string[] = [] const onBlur = vi.fn()
const rendered = await renderEditor({ render(
value: '{"count":1}', <JsonCodeEditor
onChange: (value) => changes.push(value), value='{}'
}) onChange={() => undefined}
const textarea = rendered.container.querySelector('textarea') onBlur={onBlur}
ariaLabel='Model configuration'
/>
)
assert.ok(textarea) fireEvent.blur(screen.getByRole('textbox', { name: 'Model configuration' }))
await act(async () => {
textarea.value = '{"count":2}' expect(onBlur).toHaveBeenCalledOnce()
textarea.dispatchEvent(new Event('input', { bubbles: true })) })
})
assert.deepEqual(changes, ['{"count":2}']) test('emits user edits and synchronizes a controlled value', () => {
const onChange = vi.fn()
await act(async () => { const rendered = render(
rendered.root.render( <JsonCodeEditor
<JsonCodeEditor value='{"count":1}'
value='{"count":3}' onChange={onChange}
onChange={(value) => changes.push(value)} 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 () => { test('formats valid JSON through the public toolbar action', async () => {
const changes: string[] = [] const user = userEvent.setup()
const rendered = await renderEditor({ const onChange = vi.fn()
value: '{"model":{"ratio":2}}', render(<JsonCodeEditor value='{"model":{"ratio":2}}' onChange={onChange} />)
onChange: (value) => changes.push(value),
})
const formatButton = [
...rendered.container.querySelectorAll('button'),
].find((button) => button.textContent?.includes('Format JSON'))
assert.ok(formatButton) await user.click(screen.getByRole('button', { name: 'Format JSON' }))
await act(async () => formatButton.click())
assert.deepEqual(changes, ['{\n "model": {\n "ratio": 2\n }\n}'])
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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
modelGroupSelectorLayoutClasses, modelGroupSelectorLayoutClasses,
...@@ -29,8 +28,8 @@ describe('model group selector layout', () => { ...@@ -29,8 +28,8 @@ describe('model group selector layout', () => {
const groupScrollClasses = const groupScrollClasses =
modelGroupSelectorLayoutClasses.groupScroll.split(' ') modelGroupSelectorLayoutClasses.groupScroll.split(' ')
assert.ok(groupScrollClasses.includes('auto-rows-[2rem]')) expect(groupScrollClasses.includes('auto-rows-[2rem]')).toBeTruthy()
assert.ok(groupScrollClasses.includes('content-start')) expect(groupScrollClasses.includes('content-start')).toBeTruthy()
}) })
test('centers the selected group inside its own scroll container', () => { test('centers the selected group inside its own scroll container', () => {
...@@ -50,7 +49,7 @@ describe('model group selector layout', () => { ...@@ -50,7 +49,7 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption, scrollContainer) 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', () => { test('falls back to scrollIntoView when no group container is provided', () => {
...@@ -63,6 +62,6 @@ describe('model group selector layout', () => { ...@@ -63,6 +62,6 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption) 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { handleDropdownMenuItemSelect } from './dropdown-menu-events' import { handleDropdownMenuItemSelect } from './dropdown-menu-events'
...@@ -52,8 +51,8 @@ describe('DropdownMenuItem onSelect compatibility', () => { ...@@ -52,8 +51,8 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selected = true selected = true
}) })
assert.equal(selected, true) expect(selected).toBe(true)
assert.equal(event.baseUIHandlerPrevented, false) expect(event.baseUIHandlerPrevented).toBe(false)
}) })
test('keeps the Base UI menu open when onSelect prevents default', () => { test('keeps the Base UI menu open when onSelect prevents default', () => {
...@@ -63,7 +62,7 @@ describe('DropdownMenuItem onSelect compatibility', () => { ...@@ -63,7 +62,7 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selectEvent.preventDefault() selectEvent.preventDefault()
}) })
assert.equal(event.defaultPrevented, true) expect(event.defaultPrevented).toBe(true)
assert.equal(event.baseUIHandlerPrevented, true) expect(event.baseUIHandlerPrevented).toBe(true)
}) })
}) })
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { RefreshOutcome } from '@/lib/api' import type { RefreshOutcome } from '@/lib/api'
import type { AuthBundle } from '@/stores/auth-store' import type { AuthBundle } from '@/stores/auth-store'
...@@ -63,8 +62,8 @@ describe('logout coordination', () => { ...@@ -63,8 +62,8 @@ describe('logout coordination', () => {
}, },
}) })
assert.deepEqual(result, { success: false, message: 'not revoked' }) expect(result).toEqual({ success: false, message: 'not revoked' })
assert.equal(refreshCount, 0) expect(refreshCount).toBe(0)
}) })
test('recovers a cookie mismatch and retries with the refreshed SID', async () => { test('recovers a cookie mismatch and retries with the refreshed SID', async () => {
...@@ -83,8 +82,8 @@ describe('logout coordination', () => { ...@@ -83,8 +82,8 @@ describe('logout coordination', () => {
}, },
}) })
assert.deepEqual(result, { success: true, message: '' }) expect(result).toEqual({ success: true, message: '' })
assert.deepEqual(requestedSIDs, ['session-a', 'session-b']) expect(requestedSIDs).toEqual(['session-a', 'session-b'])
}) })
test('treats a mismatch that refresh confirms anonymous as signed out', async () => { test('treats a mismatch that refresh confirms anonymous as signed out', async () => {
...@@ -96,7 +95,7 @@ describe('logout coordination', () => { ...@@ -96,7 +95,7 @@ describe('logout coordination', () => {
refresh: async () => ({ kind: 'anonymous' }), 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 () => { test('preserves the active session when mismatch recovery is temporary', async () => {
...@@ -106,15 +105,14 @@ describe('logout coordination', () => { ...@@ -106,15 +105,14 @@ describe('logout coordination', () => {
error: new Error('offline'), error: new Error('offline'),
} }
await assert.rejects( await expect(
executeLogout({ executeLogout({
getExpectedSID: () => 'session-a', getExpectedSID: () => 'session-a',
request: async () => { request: async () => {
throw originalError throw originalError
}, },
refresh: async () => transient, 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
getOAuthSessionStorage, getOAuthSessionStorage,
...@@ -40,15 +39,14 @@ const bindState = 'bind-state' ...@@ -40,15 +39,14 @@ const bindState = 'bind-state'
describe('resolveOAuthCallbackMode', () => { describe('resolveOAuthCallbackMode', () => {
test('matching provider and state mark is treated as a bind flow', () => { test('matching provider and state mark is treated as a bind flow', () => {
const storage = fakeStorage() const storage = fakeStorage()
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), true) expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(true)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'bind' ).toBe('bind')
)
}) })
// Regression: a tab opened from an external link (Slack, e-mail, another // Regression: a tab opened from an external link (Slack, e-mail, another
...@@ -58,75 +56,69 @@ describe('resolveOAuthCallbackMode', () => { ...@@ -58,75 +56,69 @@ describe('resolveOAuthCallbackMode', () => {
test('login redirect in a tab with a foreign opener stays a login flow', () => { test('login redirect in a tab with a foreign opener stays a login flow', () => {
const storage = fakeStorage() const storage = fakeStorage()
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('bind marker for another provider does not hijack this callback', () => { test('bind marker for another provider does not hijack this callback', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'github', bindState) markOAuthBindPopup(storage, 'github', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('stale bind marker does not hijack a later callback', () => { test('stale bind marker does not hijack a later callback', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', 'previous-state') markOAuthBindPopup(storage, 'oidc', 'previous-state')
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('bind marker without an opener falls back to login', () => { test('bind marker without an opener falls back to login', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState) markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: null, opener: null,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('closed opener falls back to login', () => { test('closed opener falls back to login', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState) markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: { closed: true }, opener: { closed: true },
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('missing storage degrades to login instead of throwing', () => { test('missing storage degrades to login instead of throwing', () => {
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage: null, storage: null,
}), })
'login' ).toBe('login')
)
}) })
test('storage read failure degrades to login instead of throwing', () => { test('storage read failure degrades to login instead of throwing', () => {
...@@ -137,13 +129,12 @@ describe('resolveOAuthCallbackMode', () => { ...@@ -137,13 +129,12 @@ describe('resolveOAuthCallbackMode', () => {
setItem: () => undefined, setItem: () => undefined,
} }
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
}) })
...@@ -155,7 +146,7 @@ describe('OAuth bind popup storage', () => { ...@@ -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', () => { test('marking reports unavailable or unwritable storage', () => {
...@@ -166,9 +157,9 @@ describe('OAuth bind popup storage', () => { ...@@ -166,9 +157,9 @@ describe('OAuth bind popup storage', () => {
}, },
} }
assert.equal(markOAuthBindPopup(null, 'oidc', bindState), false) expect(markOAuthBindPopup(null, 'oidc', bindState)).toBe(false)
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), false) expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(false)
assert.equal( expect(
markOAuthBindPopup( markOAuthBindPopup(
{ {
getItem: () => null, getItem: () => null,
...@@ -176,8 +167,7 @@ describe('OAuth bind popup storage', () => { ...@@ -176,8 +167,7 @@ describe('OAuth bind popup storage', () => {
}, },
'oidc', 'oidc',
bindState bindState
), )
false ).toBe(false)
)
}) })
}) })
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { AuthUser } from '@/stores/auth-store' import type { AuthUser } from '@/stores/auth-store'
...@@ -27,17 +26,15 @@ const origin = 'https://dashboard.example.com' ...@@ -27,17 +26,15 @@ const origin = 'https://dashboard.example.com'
describe('authentication redirect validation', () => { describe('authentication redirect validation', () => {
test('preserves safe internal paths, search parameters, and fragments', () => { test('preserves safe internal paths, search parameters, and fragments', () => {
assert.equal( expect(sanitizeAuthRedirect('/console?tab=usage#recent', origin)).toBe(
sanitizeAuthRedirect('/console?tab=usage#recent', origin),
'/console?tab=usage#recent' '/console?tab=usage#recent'
) )
assert.equal( expect(
sanitizeAuthRedirect( sanitizeAuthRedirect(
'https://dashboard.example.com/dashboard?tab=quota#daily', 'https://dashboard.example.com/dashboard?tab=quota#daily',
origin origin
), )
'/dashboard?tab=quota#daily' ).toBe('/dashboard?tab=quota#daily')
)
}) })
test('rejects external and ambiguously parsed redirect targets', () => { test('rejects external and ambiguously parsed redirect targets', () => {
...@@ -53,13 +50,13 @@ describe('authentication redirect validation', () => { ...@@ -53,13 +50,13 @@ describe('authentication redirect validation', () => {
] ]
for (const target of unsafeTargets) { 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', () => { test('rejects invalid or non-HTTP application origins', () => {
assert.equal(sanitizeAuthRedirect('/dashboard', 'not-an-origin'), null) expect(sanitizeAuthRedirect('/dashboard', 'not-an-origin')).toBe(null)
assert.equal(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app'), null) expect(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app')).toBe(null)
}) })
}) })
...@@ -67,31 +64,27 @@ describe('saved authentication language', () => { ...@@ -67,31 +64,27 @@ describe('saved authentication language', () => {
const user: AuthUser = { id: 1, username: 'user', role: 1 } const user: AuthUser = { id: 1, username: 'user', role: 1 }
test('prefers the explicit user language', () => { test('prefers the explicit user language', () => {
assert.equal( expect(
getSavedLanguage({ getSavedLanguage({
...user, ...user,
language: 'ja', language: 'ja',
setting: { language: 'fr' }, setting: { language: 'fr' },
}), })
'ja' ).toBe('ja')
)
}) })
test('reads object and JSON string settings', () => { test('reads object and JSON string settings', () => {
assert.equal( expect(getSavedLanguage({ ...user, setting: { language: 'fr' } })).toBe(
getSavedLanguage({ ...user, setting: { language: 'fr' } }),
'fr' 'fr'
) )
assert.equal( expect(getSavedLanguage({ ...user, setting: '{"language":"ru"}' })).toBe(
getSavedLanguage({ ...user, setting: '{"language":"ru"}' }),
'ru' 'ru'
) )
}) })
test('ignores malformed and non-string setting languages', () => { test('ignores malformed and non-string setting languages', () => {
assert.equal(getSavedLanguage({ ...user, setting: '{' }), undefined) expect(getSavedLanguage({ ...user, setting: '{' })).toBe(undefined)
assert.equal( expect(getSavedLanguage({ ...user, setting: { language: 123 } })).toBe(
getSavedLanguage({ ...user, setting: { language: 123 } }),
undefined undefined
) )
}) })
......
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
parseTelegramBindCallback, parseTelegramBindCallback,
...@@ -51,51 +50,48 @@ function fakeTimerRuntime() { ...@@ -51,51 +50,48 @@ function fakeTimerRuntime() {
describe('OAuth bind popup lifecycle', () => { describe('OAuth bind popup lifecycle', () => {
test('parses Telegram success and stable error callbacks', () => { test('parses Telegram success and stable error callbacks', () => {
assert.deepEqual( expect(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'success', telegram_bind: 'success',
flow_token: 'flow-success', flow_token: 'flow-success',
}), })
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-success', flowToken: 'flow-success',
success: true, success: true,
} })
) expect(
assert.deepEqual(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'error', telegram_bind: 'error',
flow_token: 'flow-error', flow_token: 'flow-error',
error_code: 'TELEGRAM_BIND_ALREADY_BOUND', error_code: 'TELEGRAM_BIND_ALREADY_BOUND',
}), })
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-error', flowToken: 'flow-error',
success: false, success: false,
code: 'TELEGRAM_BIND_ALREADY_BOUND', code: 'TELEGRAM_BIND_ALREADY_BOUND',
} })
)
}) })
test('rejects Telegram callbacks without a flow token and ignores descriptions', () => { 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', kind: 'invalid',
}) })
assert.deepEqual( expect(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'error', telegram_bind: 'error',
flow_token: 'flow-error', flow_token: 'flow-error',
error_code: 'UNKNOWN_CODE', error_code: 'UNKNOWN_CODE',
error_description: 'untrusted message', error_description: 'untrusted message',
} as Parameters<typeof parseTelegramBindCallback>[0]), } as Parameters<typeof parseTelegramBindCallback>[0])
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-error', flowToken: 'flow-error',
success: false, success: false,
code: 'UNKNOWN_CODE', code: 'UNKNOWN_CODE',
} })
) expect(parseTelegramBindCallback({})).toBe(null)
assert.equal(parseTelegramBindCallback({}), null)
}) })
test('posts only complete Telegram bind results to an available opener', () => { test('posts only complete Telegram bind results to an available opener', () => {
...@@ -112,11 +108,10 @@ describe('OAuth bind popup lifecycle', () => { ...@@ -112,11 +108,10 @@ describe('OAuth bind popup lifecycle', () => {
error_code: 'UNKNOWN_CODE', error_code: 'UNKNOWN_CODE',
}) })
assert.equal( expect(
postTelegramBindResult(callback, opener, 'https://dashboard.example.com'), postTelegramBindResult(callback, opener, 'https://dashboard.example.com')
true ).toBe(true)
) expect(messages).toEqual([
assert.deepEqual(messages, [
{ {
message: { message: {
type: 'telegram:binding:result', type: 'telegram:binding:result',
...@@ -128,23 +123,17 @@ describe('OAuth bind popup lifecycle', () => { ...@@ -128,23 +123,17 @@ describe('OAuth bind popup lifecycle', () => {
}, },
]) ])
assert.equal( expect(
postTelegramBindResult( postTelegramBindResult({ kind: 'invalid' }, opener, 'https://example.com')
{ kind: 'invalid' }, ).toBe(false)
opener, expect(
'https://example.com'
),
false
)
assert.equal(
postTelegramBindResult( postTelegramBindResult(
callback, callback,
{ ...opener, closed: true }, { ...opener, closed: true },
'https://example.com' 'https://example.com'
), )
false ).toBe(false)
) expect(messages.length).toBe(1)
assert.equal(messages.length, 1)
}) })
test('waits 30 seconds for the opener response and can be cancelled', () => { test('waits 30 seconds for the opener response and can be cancelled', () => {
...@@ -158,11 +147,11 @@ describe('OAuth bind popup lifecycle', () => { ...@@ -158,11 +147,11 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime timer.runtime
) )
assert.equal(timer.delay, 30_000) expect(timer.delay).toBe(30_000)
cancel() cancel()
timer.fire() timer.fire()
assert.equal(timedOut, false) expect(timedOut).toBe(false)
assert.deepEqual(timer.cancelled, [timer.handle]) expect(timer.cancelled).toEqual([timer.handle])
}) })
test('reports a closed popup once and clears its poller', () => { test('reports a closed popup once and clears its poller', () => {
...@@ -178,13 +167,13 @@ describe('OAuth bind popup lifecycle', () => { ...@@ -178,13 +167,13 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime timer.runtime
) )
assert.equal(timer.delay, 500) expect(timer.delay).toBe(500)
timer.fire() timer.fire()
assert.equal(closedCount, 0) expect(closedCount).toBe(0)
popup.closed = true popup.closed = true
timer.fire() timer.fire()
timer.fire() timer.fire()
assert.equal(closedCount, 1) expect(closedCount).toBe(1)
assert.deepEqual(timer.cancelled, [timer.handle]) expect(timer.cancelled).toEqual([timer.handle])
}) })
}) })
...@@ -16,14 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,14 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { pickTelegramAuthorization } from './telegram-login' import { pickTelegramAuthorization } from './telegram-login'
describe('Telegram login authorization', () => { describe('Telegram login authorization', () => {
test('keeps only fields signed by the Telegram login contract', () => { test('keeps only fields signed by the Telegram login contract', () => {
assert.deepEqual( expect(
pickTelegramAuthorization({ pickTelegramAuthorization({
id: 12345, id: 12345,
first_name: 'Test', first_name: 'Test',
...@@ -35,33 +34,27 @@ describe('Telegram login authorization', () => { ...@@ -35,33 +34,27 @@ describe('Telegram login authorization', () => {
lang: 'en', lang: 'en',
admin: true, admin: true,
redirect: 'https://attacker.example', redirect: 'https://attacker.example',
}), })
{ ).toEqual({
id: 12345, id: 12345,
first_name: 'Test', first_name: 'Test',
last_name: 'User', last_name: 'User',
username: 'test_user', username: 'test_user',
photo_url: 'https://t.me/i/userpic/320/test.jpg', photo_url: 'https://t.me/i/userpic/320/test.jpg',
auth_date: 1_900_000_000, auth_date: 1_900_000_000,
hash: 'signed-hash', hash: 'signed-hash',
lang: 'en', lang: 'en',
} })
)
}) })
test('rejects incomplete or structurally invalid callbacks', () => { test('rejects incomplete or structurally invalid callbacks', () => {
assert.equal(pickTelegramAuthorization(null), null) expect(pickTelegramAuthorization(null)).toBe(null)
assert.equal( expect(pickTelegramAuthorization({ auth_date: 1, hash: 'hash' })).toBe(null)
pickTelegramAuthorization({ auth_date: 1, hash: 'hash' }), expect(pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' })).toBe(
null
)
assert.equal(
pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' }),
null
)
assert.equal(
pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' }),
null 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
CHANNEL_FIELD_UPDATE_DELAY_MS, CHANNEL_FIELD_UPDATE_DELAY_MS,
...@@ -31,7 +30,7 @@ function createFakeTimers() { ...@@ -31,7 +30,7 @@ function createFakeTimers() {
return { return {
timers: { timers: {
setTimeout: (callback: () => void, delay: number) => { setTimeout: (callback: () => void, delay: number) => {
assert.equal(delay, CHANNEL_FIELD_UPDATE_DELAY_MS) expect(delay).toBe(CHANNEL_FIELD_UPDATE_DELAY_MS)
const id = nextId++ const id = nextId++
pending.set(id, callback) pending.set(id, callback)
return id return id
...@@ -63,11 +62,11 @@ describe('channel field update scheduler', () => { ...@@ -63,11 +62,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(1) scheduler.schedule(1)
scheduler.schedule(2) scheduler.schedule(2)
scheduler.schedule(3) scheduler.schedule(3)
assert.deepEqual(updates, []) expect(updates).toEqual([])
assert.equal(fake.pendingCount, 1) expect(fake.pendingCount).toBe(1)
fake.fireAll() fake.fireAll()
assert.deepEqual(updates, [3]) expect(updates).toEqual([3])
}) })
test('flush commits the pending value immediately and cancels the timer', () => { test('flush commits the pending value immediately and cancels the timer', () => {
...@@ -80,11 +79,11 @@ describe('channel field update scheduler', () => { ...@@ -80,11 +79,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(7) scheduler.schedule(7)
scheduler.flush() scheduler.flush()
assert.deepEqual(updates, [7]) expect(updates).toEqual([7])
assert.equal(fake.pendingCount, 0) expect(fake.pendingCount).toBe(0)
fake.fireAll() fake.fireAll()
assert.deepEqual(updates, [7]) expect(updates).toEqual([7])
}) })
test('flush without a pending value does nothing', () => { test('flush without a pending value does nothing', () => {
...@@ -99,7 +98,7 @@ describe('channel field update scheduler', () => { ...@@ -99,7 +98,7 @@ describe('channel field update scheduler', () => {
scheduler.schedule(5) scheduler.schedule(5)
scheduler.flush() scheduler.flush()
scheduler.flush() scheduler.flush()
assert.deepEqual(updates, [5]) expect(updates).toEqual([5])
}) })
test('preserves a pending value of 0', () => { test('preserves a pending value of 0', () => {
...@@ -112,6 +111,6 @@ describe('channel field update scheduler', () => { ...@@ -112,6 +111,6 @@ describe('channel field update scheduler', () => {
scheduler.schedule(0) scheduler.schedule(0)
scheduler.flush() 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { Channel } from '../../types' import type { Channel } from '../../types'
import { getChannelTableRowId, type TagRow } from '../channel-utils' import { getChannelTableRowId, type TagRow } from '../channel-utils'
...@@ -35,12 +34,8 @@ describe('channel table row identity', () => { ...@@ -35,12 +34,8 @@ describe('channel table row identity', () => {
const beforeUpdate = [first, updated, third].map(getChannelTableRowId) const beforeUpdate = [first, updated, third].map(getChannelTableRowId)
const afterUpdate = [updated, first, third].map(getChannelTableRowId) const afterUpdate = [updated, first, third].map(getChannelTableRowId)
assert.deepEqual(beforeUpdate, [ expect(beforeUpdate).toEqual(['channel:101', 'channel:202', 'channel:303'])
'channel:101', expect(afterUpdate).toEqual(['channel:202', 'channel:101', 'channel:303'])
'channel:202',
'channel:303',
])
assert.deepEqual(afterUpdate, ['channel:202', 'channel:101', 'channel:303'])
}) })
test('uses separate namespaces for tag and channel rows', () => { test('uses separate namespaces for tag and channel rows', () => {
...@@ -50,7 +45,7 @@ describe('channel table row identity', () => { ...@@ -50,7 +45,7 @@ describe('channel table row identity', () => {
children: [channel(202)], children: [channel(202)],
} as TagRow } as TagRow
assert.equal(getChannelTableRowId(tagRow), 'tag:202') expect(getChannelTableRowId(tagRow)).toBe('tag:202')
assert.equal(getChannelTableRowId(channel(202)), 'channel:202') expect(getChannelTableRowId(channel(202))).toBe('channel:202')
}) })
}) })
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
CHANNEL_TYPE_NEW_API, CHANNEL_TYPE_NEW_API,
...@@ -45,45 +44,40 @@ describe('New API channel', () => { ...@@ -45,45 +44,40 @@ describe('New API channel', () => {
(item) => item.value === CHANNEL_TYPE_NEW_API (item) => item.value === CHANNEL_TYPE_NEW_API
) )
assert.deepEqual(option, { expect(option).toEqual({
value: CHANNEL_TYPE_NEW_API, value: CHANNEL_TYPE_NEW_API,
label: 'New API', label: 'New API',
}) })
assert.equal( expect(
CHANNEL_TYPE_OPTIONS.findIndex( CHANNEL_TYPE_OPTIONS.findIndex(
(item) => item.value === CHANNEL_TYPE_NEW_API (item) => item.value === CHANNEL_TYPE_NEW_API
) + 1, ) + 1
CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58) ).toBe(CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58))
) expect(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API)).toBe(true)
assert.equal(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API), true) expect(getChannelTypeIcon(CHANNEL_TYPE_NEW_API)).toBe('NewAPI')
assert.equal(getChannelTypeIcon(CHANNEL_TYPE_NEW_API), 'NewAPI') expect(getKeyPromptForType(CHANNEL_TYPE_NEW_API)).toBe(
assert.equal(
getKeyPromptForType(CHANNEL_TYPE_NEW_API),
'Enter API key for this channel' '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', () => { test('requires a non-blank Base URL', () => {
const blankResult = channelFormSchema.safeParse(newAPIForm(' ')) const blankResult = channelFormSchema.safeParse(newAPIForm(' '))
assert.equal(blankResult.success, false) expect(blankResult.success).toBe(false)
if (!blankResult.success) { if (!blankResult.success) {
assert.equal( expect(
blankResult.error.issues.some( blankResult.error.issues.some(
(issue) => (issue) =>
issue.path[0] === 'base_url' && issue.path[0] === 'base_url' &&
issue.message === 'Base URL is required for this channel type' issue.message === 'Base URL is required for this channel type'
), )
true ).toBe(true)
)
} }
assert.equal( expect(
channelFormSchema.safeParse(newAPIForm('https://new-api.example')) channelFormSchema.safeParse(newAPIForm('https://new-api.example')).success
.success, ).toBe(true)
true
)
}) })
test('keeps Sub2API Base URL validation unchanged', () => { test('keeps Sub2API Base URL validation unchanged', () => {
...@@ -92,6 +86,6 @@ describe('New API channel', () => { ...@@ -92,6 +86,6 @@ describe('New API channel', () => {
type: 59, 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { FlowUserFilterOption } from '../types' import type { FlowUserFilterOption } from '../types'
import { import {
...@@ -46,89 +45,75 @@ const users: FlowUserFilterOption[] = [ ...@@ -46,89 +45,75 @@ const users: FlowUserFilterOption[] = [
describe('dashboard flow selection helpers', () => { describe('dashboard flow selection helpers', () => {
test('limits user chips to currently visible users', () => { test('limits user chips to currently visible users', () => {
assert.deepEqual( expect(visibleFlowUsers(users, []).map((user) => user.value)).toEqual([
visibleFlowUsers(users, []).map((user) => user.value), 'user:1',
['user:1', 'user:2'] 'user:2',
) ])
assert.deepEqual( expect(
visibleFlowUsers(users, ['user:2']).map((user) => user.value), visibleFlowUsers(users, ['user:2']).map((user) => user.value)
['user:2'] ).toEqual(['user:2'])
)
}) })
test('filters visible users without mutating the source options', () => { test('filters visible users without mutating the source options', () => {
const visible = visibleFlowUsers(users, ['user:1']) const visible = visibleFlowUsers(users, ['user:1'])
assert.deepEqual( expect(visible.map((user) => user.value)).toEqual(['user:1'])
visible.map((user) => user.value), expect(users.map((user) => user.value)).toEqual(['user:1', 'user:2'])
['user:1']
)
assert.deepEqual(
users.map((user) => user.value),
['user:1', 'user:2']
)
}) })
test('formats compact selected counts for flow multiselect summaries', () => { test('formats compact selected counts for flow multiselect summaries', () => {
assert.equal(compactFlowSelectionLabel(0), '*') expect(compactFlowSelectionLabel(0)).toBe('*')
assert.equal(compactFlowSelectionLabel(1), '1') expect(compactFlowSelectionLabel(1)).toBe('1')
assert.equal(compactFlowSelectionLabel(23), '23') expect(compactFlowSelectionLabel(23)).toBe('23')
}) })
test('prioritizes loading and error states before empty flow data', () => { test('prioritizes loading and error states before empty flow data', () => {
assert.equal( expect(
flowDisplayState({ flowDisplayState({
isLoading: true, isLoading: true,
isError: true, isError: true,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'loading' ).toBe('loading')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: true, isError: true,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'error' ).toBe('error')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: false, isError: false,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'empty' ).toBe('empty')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: false, isError: false,
linkCount: 1, linkCount: 1,
themeReady: false, themeReady: false,
}), })
'loading' ).toBe('loading')
)
}) })
test('throws unsuccessful flow responses instead of treating them as empty data', () => { test('throws unsuccessful flow responses instead of treating them as empty data', () => {
assert.throws( expect(() =>
() => requireSuccessfulFlowRows(
requireSuccessfulFlowRows( { success: false, data: [], message: 'database unavailable' },
{ success: false, data: [], message: 'database unavailable' }, 'Failed to load'
'Failed to load' )
), ).toThrow(/database unavailable/)
/database unavailable/ expect(
)
assert.deepEqual(
requireSuccessfulFlowRows( requireSuccessfulFlowRows(
{ success: true, data: [{ user_id: 1, quota: 10 }] }, { success: true, data: [{ user_id: 1, quota: 10 }] },
'Failed to load' '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/>. ...@@ -16,39 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { render } from '@testing-library/react'
import { after, describe, test } from 'node:test' import { describe, expect, test } from 'vitest'
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],
})
}
const { act } = await import('react')
const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next') const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { TooltipProvider } = await import('@/components/ui/tooltip') const { TooltipProvider } = await import('@/components/ui/tooltip')
...@@ -70,11 +40,6 @@ await i18n.use(initReactI18next).init({ ...@@ -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: { function CellHarness(props: {
group: string group: string
ratio?: number | string ratio?: number | string
...@@ -96,141 +61,93 @@ function CellHarness(props: { ...@@ -96,141 +61,93 @@ function CellHarness(props: {
} }
describe('API key group table cell', () => { describe('API key group table cell', () => {
after(() => { test('renders an unclipped ring and a localized Auto ratio when API data uses a nonlocalized string', () => {
domWindow.close() const { container } = render(
}) <CellHarness
group='auto'
test('renders two unclipped rings and a localized Auto ratio when API data uses a nonlocalized string', async () => { ratio='自动'
const container = document.createElement('div') crossGroupRetry
document.body.append(container) shouldReduceMotion={false}
const root = createRoot(container) />
await act(async () =>
root.render(
<CellHarness
group='auto'
ratio='自动'
crossGroupRetry
shouldReduceMotion={false}
/>
)
) )
const badgeCell = container.querySelector<HTMLElement>( const badgeCell = container.querySelector<HTMLElement>(
'[data-api-key-group-cell="auto"]' '[data-api-key-group-cell="auto"]'
) )
assert.ok(badgeCell) expect(badgeCell).toHaveClass('overflow-visible')
assert.equal(badgeCell.classList.contains('overflow-visible'), true) expect(badgeCell).not.toHaveClass('overflow-hidden')
assert.equal(badgeCell.classList.contains('overflow-hidden'), false)
const frames = container.querySelectorAll('[data-auto-group-frame]') const frames = container.querySelectorAll('[data-auto-group-frame]')
const movingRings = container.querySelectorAll( const movingRings = container.querySelectorAll(
'[data-auto-group-flow-border]' '[data-auto-group-flow-border]'
) )
assert.equal(frames.length, 2) expect(frames.length).toBe(1)
assert.equal(movingRings.length, 2) expect(movingRings.length).toBe(1)
for (const frame of frames) { for (const frame of frames) {
assert.equal(frame.classList.contains('relative'), true) expect(frame).toHaveClass(
assert.equal(frame.classList.contains('overflow-visible'), true) 'relative',
assert.equal(frame.classList.contains('rounded-4xl'), true) 'overflow-visible',
assert.equal(frame.classList.contains('p-px'), true) 'rounded-4xl',
'p-px'
)
} }
const ratio = container.querySelector<HTMLElement>( const ratio = container.querySelector<HTMLElement>(
'[data-auto-group-effect="ratio"]' '[data-auto-group-effect="ratio"]'
) )
assert.ok(ratio) expect(ratio).toHaveTextContent('Auto Ratio')
assert.equal(ratio.textContent, 'Auto Ratio') expect(ratio).not.toHaveTextContent('x')
assert.equal(ratio.textContent?.includes('x'), false) expect(container).not.toHaveTextContent('自动')
assert.equal(container.textContent?.includes('自动'), false) expect(container).toHaveTextContent('Cross-group')
assert.equal(container.textContent?.includes('Cross-group'), true)
const crossGroupBadge = [ const crossGroupBadge = [
...container.querySelectorAll<HTMLElement>('[data-slot="status-badge"]'), ...container.querySelectorAll<HTMLElement>('[data-slot="status-badge"]'),
].find((badge) => badge.textContent === 'Cross-group') ].find((badge) => badge.textContent === 'Cross-group')
assert.ok(crossGroupBadge) expect(crossGroupBadge).not.toBeUndefined()
assert.equal(crossGroupBadge.closest('[data-auto-group-frame]'), null) expect(crossGroupBadge?.closest('[data-auto-group-frame]')).toBeNull()
await act(async () => root.unmount())
container.remove()
}) })
test('keeps static Auto frames but omits both moving layers for reduced motion', async () => { test('keeps the static Auto ratio frame but omits its moving layer for reduced motion', () => {
const container = document.createElement('div') const { container } = render(
document.body.append(container) <CellHarness group='auto' ratio='Auto' shouldReduceMotion />
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' ratio='Auto' shouldReduceMotion />)
) )
assert.equal( expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(1)
container.querySelectorAll('[data-auto-group-frame]').length, expect(
2 container.querySelectorAll('[data-auto-group-flow-border]').length
) ).toBe(0)
assert.equal(
container.querySelectorAll('[data-auto-group-flow-border]').length,
0
)
await act(async () => root.unmount())
container.remove()
}) })
test('shows only the Auto badge when ratio data is unavailable', async () => { test('shows only the cross-group badge when ratio data is unavailable', () => {
const container = document.createElement('div') const { container } = render(
document.body.append(container) <CellHarness group='auto' shouldReduceMotion={false} />
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' shouldReduceMotion={false} />)
) )
assert.equal( expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(0)
container.querySelectorAll('[data-auto-group-frame]').length, expect(
1 container.querySelectorAll('[data-auto-group-flow-border]').length
) ).toBe(0)
assert.equal( expect(container.querySelector('[data-auto-group-effect="ratio"]')).toBe(
container.querySelectorAll('[data-auto-group-flow-border]').length,
1
)
assert.equal(
container.querySelector('[data-auto-group-effect="ratio"]'),
null null
) )
assert.equal(container.textContent?.includes('Auto'), true) expect(container).toHaveTextContent('Cross-group')
assert.equal(container.textContent?.includes('Ratio'), false) expect(container).not.toHaveTextContent('Auto')
expect(container).not.toHaveTextContent('Ratio')
await act(async () => root.unmount())
container.remove()
}) })
test('narrows normal group ratios to numbers and never applies Auto rings', async () => { test('narrows normal group ratios to numbers and never applies Auto rings', () => {
const container = document.createElement('div') const { container, rerender } = render(
document.body.append(container) <CellHarness group='vip' ratio='自动' shouldReduceMotion={false} />
const root = createRoot(container)
await act(async () =>
root.render(
<CellHarness group='vip' ratio='自动' shouldReduceMotion={false} />
)
) )
assert.equal(container.textContent?.includes('vip'), true) expect(container).toHaveTextContent('vip')
assert.equal(container.textContent?.includes('自动'), false) expect(container).not.toHaveTextContent('自动')
assert.equal(container.querySelector('[data-auto-group-frame]'), null) expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
assert.equal(container.querySelector('[data-auto-group-flow-border]'), null) expect(container.querySelector('[data-auto-group-flow-border]')).toBe(null)
await act(async () =>
root.render(
<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />
)
)
assert.equal(container.textContent?.includes('3x'), true) rerender(<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />)
assert.equal(container.querySelector('[data-auto-group-frame]'), null)
await act(async () => root.unmount()) expect(container).toHaveTextContent('3x')
container.remove() 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/>. ...@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 type { TFunction } from 'i18next'
import { describe, expect, test } from 'vitest'
import { apiKeySchema, type ApiKey } from '../../types' import { apiKeySchema, type ApiKey } from '../../types'
import { import {
...@@ -60,16 +58,16 @@ describe('API key Auto group form mapping', () => { ...@@ -60,16 +58,16 @@ describe('API key Auto group form mapping', () => {
const legacyApiKey: Record<string, unknown> = { ...baseApiKey } const legacyApiKey: Record<string, unknown> = { ...baseApiKey }
delete legacyApiKey.auto_groups 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', () => { test('creates an Auto token that inherits the global order', () => {
const defaults = getApiKeyFormDefaultValues(true) const defaults = getApiKeyFormDefaultValues(true)
assert.equal(defaults.group, 'auto') expect(defaults.group).toBe('auto')
assert.equal(defaults.auto_groups_mode, 'inherit') expect(defaults.auto_groups_mode).toBe('inherit')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
assert.deepEqual(transformFormDataToPayload(defaults).auto_groups, []) expect(transformFormDataToPayload(defaults).auto_groups).toEqual([])
}) })
test('maps omitted, null, and empty snapshots to inheritance on edit', () => { test('maps omitted, null, and empty snapshots to inheritance on edit', () => {
...@@ -88,8 +86,8 @@ describe('API key Auto group form mapping', () => { ...@@ -88,8 +86,8 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'inherit') expect(defaults.auto_groups_mode).toBe('inherit')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
} }
}) })
...@@ -103,8 +101,8 @@ describe('API key Auto group form mapping', () => { ...@@ -103,8 +101,8 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'custom') expect(defaults.auto_groups_mode).toBe('custom')
assert.deepEqual(defaults.auto_groups, ['vip', 'default']) expect(defaults.auto_groups).toEqual(['vip', 'default'])
}) })
test('keeps a fully filtered snapshot custom and rejects it until resolved', () => { test('keeps a fully filtered snapshot custom and rejects it until resolved', () => {
...@@ -114,15 +112,14 @@ describe('API key Auto group form mapping', () => { ...@@ -114,15 +112,14 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'custom') expect(defaults.auto_groups_mode).toBe('custom')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
const result = getApiKeyFormSchema(t, 2).safeParse(defaults) const result = getApiKeyFormSchema(t, 2).safeParse(defaults)
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.deepEqual(result.error.issues[0]?.path, ['auto_groups']) expect(result.error.issues[0]?.path).toEqual(['auto_groups'])
assert.equal( expect(result.error.issues[0]?.message).toBe(
result.error.issues[0]?.message,
'Select at least one Auto group or restore global Auto.' 'Select at least one Auto group or restore global Auto.'
) )
}) })
...@@ -134,7 +131,7 @@ describe('API key Auto group form mapping', () => { ...@@ -134,7 +131,7 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'default'], auto_groups: ['vip', 'default'],
} }
assert.deepEqual(transformFormDataToPayload(custom).auto_groups, [ expect(transformFormDataToPayload(custom).auto_groups).toEqual([
'vip', 'vip',
'default', 'default',
]) ])
...@@ -142,7 +139,7 @@ describe('API key Auto group form mapping', () => { ...@@ -142,7 +139,7 @@ describe('API key Auto group form mapping', () => {
test('submits an empty array for inheritance and for non-Auto groups', () => { test('submits an empty array for inheritance and for non-Auto groups', () => {
const inherited = getApiKeyFormDefaultValues(true) const inherited = getApiKeyFormDefaultValues(true)
assert.deepEqual(transformFormDataToPayload(inherited).auto_groups, []) expect(transformFormDataToPayload(inherited).auto_groups).toEqual([])
const nonAuto = { const nonAuto = {
...inherited, ...inherited,
...@@ -150,8 +147,8 @@ describe('API key Auto group form mapping', () => { ...@@ -150,8 +147,8 @@ describe('API key Auto group form mapping', () => {
auto_groups_mode: 'custom' as const, auto_groups_mode: 'custom' as const,
auto_groups: ['vip'], auto_groups: ['vip'],
} }
assert.deepEqual(transformFormDataToPayload(nonAuto).auto_groups, []) expect(transformFormDataToPayload(nonAuto).auto_groups).toEqual([])
assert.equal(transformFormDataToPayload(nonAuto).cross_group_retry, false) expect(transformFormDataToPayload(nonAuto).cross_group_retry).toBe(false)
}) })
test('rejects snapshots over the configured limit', () => { test('rejects snapshots over the configured limit', () => {
...@@ -162,13 +159,10 @@ describe('API key Auto group form mapping', () => { ...@@ -162,13 +159,10 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['default', 'vip'], auto_groups: ['default', 'vip'],
}) })
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.equal(result.error.issues[0]?.path[0], 'auto_groups') expect(result.error.issues[0]?.path[0]).toBe('auto_groups')
assert.equal( expect(result.error.issues[0]?.message).toBe('Select at most 1 Auto groups')
result.error.issues[0]?.message,
'Select at most 1 Auto groups'
)
}) })
test('rejects duplicate custom groups', () => { test('rejects duplicate custom groups', () => {
...@@ -179,10 +173,9 @@ describe('API key Auto group form mapping', () => { ...@@ -179,10 +173,9 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'vip'], auto_groups: ['vip', 'vip'],
}) })
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.equal( expect(result.error.issues[0]?.message).toBe(
result.error.issues[0]?.message,
'Auto groups must not contain duplicates' 'Auto groups must not contain duplicates'
) )
}) })
......
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { ChatCompletionRequest } from '../types' import type { ChatCompletionRequest } from '../types'
import { createStreamRequestController } from './use-stream-request' import { createStreamRequestController } from './use-stream-request'
...@@ -103,12 +102,12 @@ describe('latest-wins stream request coordination', () => { ...@@ -103,12 +102,12 @@ describe('latest-wins stream request coordination', () => {
const second = controller.send(payload, noopCallbacks) const second = controller.send(payload, noopCallbacks)
firstHeaders.resolve({ Authorization: 'Bearer stale' }) firstHeaders.resolve({ Authorization: 'Bearer stale' })
await first await first
assert.equal(sources.length, 0) expect(sources.length).toBe(0)
secondHeaders.resolve({ Authorization: 'Bearer current' }) secondHeaders.resolve({ Authorization: 'Bearer current' })
await second await second
assert.equal(sources.length, 1) expect(sources.length).toBe(1)
assert.equal(sources[0]?.streamed, true) expect(sources[0]?.streamed).toBe(true)
}) })
test('stop cancels a request that is still waiting for headers', async () => { test('stop cancels a request that is still waiting for headers', async () => {
...@@ -128,7 +127,7 @@ describe('latest-wins stream request coordination', () => { ...@@ -128,7 +127,7 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' }) headers.resolve({ Authorization: 'Bearer ignored' })
await request await request
assert.equal(sourceCount, 0) expect(sourceCount).toBe(0)
}) })
test('dispose cancels a pending header request without a state update', async () => { test('dispose cancels a pending header request without a state update', async () => {
...@@ -149,8 +148,8 @@ describe('latest-wins stream request coordination', () => { ...@@ -149,8 +148,8 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' }) headers.resolve({ Authorization: 'Bearer ignored' })
await request await request
assert.equal(sourceCount, 0) expect(sourceCount).toBe(0)
assert.deepEqual(streamingStates, [false]) expect(streamingStates).toEqual([false])
}) })
test('closes the previous source and ignores all of its later events', async () => { test('closes the previous source and ignores all of its later events', async () => {
...@@ -182,7 +181,7 @@ describe('latest-wins stream request coordination', () => { ...@@ -182,7 +181,7 @@ describe('latest-wins stream request coordination', () => {
await controller.send(payload, callbacks) await controller.send(payload, callbacks)
const second = 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( sources[0]?.emit(
'message', 'message',
JSON.stringify({ choices: [{ delta: { content: 'stale' } }] }) JSON.stringify({ choices: [{ delta: { content: 'stale' } }] })
...@@ -195,6 +194,6 @@ describe('latest-wins stream request coordination', () => { ...@@ -195,6 +194,6 @@ describe('latest-wins stream request coordination', () => {
JSON.stringify({ choices: [{ delta: { content: 'current' } }] }) JSON.stringify({ choices: [{ delta: { content: 'current' } }] })
) )
assert.deepEqual(updates, ['current']) expect(updates).toEqual(['current'])
}) })
}) })
import type { TFunction } from 'i18next'
/* /*
Copyright (C) 2023-2026 QuantumNous Copyright (C) 2023-2026 QuantumNous
...@@ -16,10 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,10 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { TFunction } from 'i18next'
import { loginMethodLabel, sessionDevice } from '../login-session-utils' import { loginMethodLabel, sessionDevice } from '../login-session-utils'
...@@ -27,14 +25,10 @@ const translate = ((key: string) => key) as TFunction ...@@ -27,14 +25,10 @@ const translate = ((key: string) => key) as TFunction
describe('login session presentation', () => { describe('login session presentation', () => {
test('labels built-in and provider OAuth login methods', () => { test('labels built-in and provider OAuth login methods', () => {
assert.equal(loginMethodLabel('password', translate), 'Password') expect(loginMethodLabel('password', translate)).toBe('Password')
assert.equal( expect(loginMethodLabel('2fa', translate)).toBe('Two-factor Authentication')
loginMethodLabel('2fa', translate), expect(loginMethodLabel('oauth:github', translate)).toBe('OAuth · GitHub')
'Two-factor Authentication' expect(loginMethodLabel('oauth:custom-provider', translate)).toBe(
)
assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
assert.equal(
loginMethodLabel('oauth:custom-provider', translate),
'OAuth · custom-provider' 'OAuth · custom-provider'
) )
}) })
...@@ -43,8 +37,7 @@ describe('login session presentation', () => { ...@@ -43,8 +37,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
'Safari · iOS' 'Safari · iOS'
) )
}) })
...@@ -53,8 +46,7 @@ describe('login session presentation', () => { ...@@ -53,8 +46,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
'Safari · iOS' 'Safari · iOS'
) )
}) })
...@@ -63,8 +55,7 @@ describe('login session presentation', () => { ...@@ -63,8 +55,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 10)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 10),
'Chrome · Windows' 'Chrome · Windows'
) )
}) })
...@@ -73,8 +64,7 @@ describe('login session presentation', () => { ...@@ -73,8 +64,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
'Chrome · Android' 'Chrome · Android'
) )
}) })
...@@ -83,15 +73,13 @@ describe('login session presentation', () => { ...@@ -83,15 +73,13 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
'Safari · macOS' 'Safari · macOS'
) )
}) })
test('falls back to the unknown-device label for an empty user agent', () => { test('falls back to the unknown-device label for an empty user agent', () => {
assert.equal( expect(sessionDevice('', 'Unknown device', 'Browser')).toBe(
sessionDevice('', 'Unknown device', 'Browser'),
'Unknown device' 'Unknown device'
) )
}) })
......
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { positiveIntegerSchema } from '../../utils/numeric-field' import { positiveIntegerSchema } from '../../utils/numeric-field'
...@@ -26,15 +25,15 @@ const schema = positiveIntegerSchema(t('Enter a positive integer')) ...@@ -26,15 +25,15 @@ const schema = positiveIntegerSchema(t('Enter a positive integer'))
describe('per-token Auto group limit validation', () => { describe('per-token Auto group limit validation', () => {
test('accepts any positive integer without a product upper bound', () => { 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', () => { test('rejects zero, negative, and fractional limits', () => {
for (const maxTokenAutoGroups of [0, -1, 1.5]) { for (const maxTokenAutoGroups of [0, -1, 1.5]) {
const result = schema.safeParse(maxTokenAutoGroups) const result = schema.safeParse(maxTokenAutoGroups)
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) continue 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/>. ...@@ -16,128 +16,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { after, describe, test } from 'node:test' 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: {
'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', () => { describe('tool price validation', () => {
after(() => { beforeAll(() => {
domWindow.close() i18next.addResourceBundle('en', 'translation', {
'Price ($/1K calls)': 'Price ($/1K calls)',
'Please enter a valid number': 'Please enter a valid number',
'Tool identifier': 'Tool identifier',
})
}) })
test('blocks an empty price without converting it to an explicit zero', async () => { test('blocks an empty price without converting it to an explicit zero', () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } }, defaultOptions: { mutations: { retry: false } },
}) })
await act(async () => { render(
root.render( <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <ToolPriceSettings defaultValue='{"web_search":10}' />
<I18nextProvider i18n={i18n}> </QueryClientProvider>
<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 () => { const priceInput = screen.getByRole('spinbutton', {
changeInputValue(priceInput, '') name: 'Price ($/1K calls): web_search',
}) })
const saveButton = screen.getByRole('button', { name: 'Save tool prices' })
assert.equal(priceInput.getAttribute('aria-invalid'), 'true') fireEvent.change(priceInput, { target: { value: '' } })
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)
await act(async () => { expect(priceInput).toHaveAttribute('aria-invalid', 'true')
changeInputValue(priceInput, '0') 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') expect(priceInput).toHaveAttribute('aria-invalid', 'false')
assert.equal(saveButton.disabled, false) expect(saveButton).toBeEnabled()
await act(async () => root.unmount())
container.remove()
queryClient.clear() queryClient.clear()
}) })
}) })
...@@ -16,88 +16,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,88 +16,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { render, screen } from '@testing-library/react'
import { after, describe, test } from 'node:test' import i18next from 'i18next'
import { Window } from 'happy-dom'
import type React from 'react' import type React from 'react'
import { beforeAll, describe, expect, test } from 'vitest'
const domWindow = new Window() import { formatLogQuota } from '@/lib/format'
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
type RenderedCost = { import { LogCostDisplay } from '../log-cost-display'
container: HTMLDivElement
root: ReturnType<typeof createRoot>
}
async function renderCost( function renderCost(
props: React.ComponentProps<typeof LogCostDisplay> props: React.ComponentProps<typeof LogCostDisplay>
): Promise<RenderedCost> { ): ReturnType<typeof render> {
const container = document.createElement('div') return render(<LogCostDisplay {...props} />)
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()
} }
function normalizedText(value: string | null): string { function normalizedText(value: string | null): string {
...@@ -105,39 +36,36 @@ function normalizedText(value: string | null): string { ...@@ -105,39 +36,36 @@ function normalizedText(value: string | null): string {
} }
describe('log cost display', () => { describe('log cost display', () => {
after(() => { beforeAll(() => {
domWindow.close() 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 () => { test('keeps the regular cost visible and adds an accessible surcharge marker', () => {
const rendered = await renderCost({ const rendered = renderCost({
quota: 12500, quota: 12500,
other: { other: {
tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }], tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }],
}, },
}) })
assert.equal( expect(
normalizedText(rendered.container.textContent).includes( normalizedText(rendered.container.textContent).includes(
normalizedText(formatLogQuota(12500)) normalizedText(formatLogQuota(12500))
), )
true ).toBe(true)
) const marker = screen.getByRole('img', {
const marker = rendered.container.querySelector( name: 'Includes tool-call surcharge',
'[data-tool-surcharge-indicator="true"]' })
) expect(marker).toHaveAttribute('data-tool-surcharge-indicator', 'true')
assert.ok(marker) expect(marker).toHaveAttribute('tabindex', '0')
assert.equal(
marker.getAttribute('aria-label'),
'Includes tool-call surcharge'
)
assert.equal(marker.getAttribute('tabindex'), '0')
await unmountCost(rendered)
}) })
test('preserves the subscription badge and adds the same legacy surcharge marker', async () => { test('preserves the subscription badge and adds the same legacy surcharge marker', () => {
const rendered = await renderCost({ renderCost({
quota: 5000, quota: 5000,
other: { other: {
billing_source: 'subscription', billing_source: 'subscription',
...@@ -147,11 +75,9 @@ describe('log cost display', () => { ...@@ -147,11 +75,9 @@ describe('log cost display', () => {
}, },
}) })
assert.equal(rendered.container.textContent?.includes('Subscription'), true) expect(screen.getByText('Subscription')).toBeInTheDocument()
assert.ok( expect(
rendered.container.querySelector('[data-tool-surcharge-indicator="true"]') screen.getByRole('img', { name: 'Includes tool-call surcharge' })
) ).toHaveAttribute('data-tool-surcharge-indicator', 'true')
await unmountCost(rendered)
}) })
}) })
...@@ -16,20 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,20 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { LogOtherData } from '../../types' import type { LogOtherData } from '../../types'
import { hasToolSurcharge } from '../format' import { hasToolSurcharge } from '../format'
describe('tool surcharge detection', () => { describe('tool surcharge detection', () => {
test('shows the marker for a charged structured tool surcharge', () => { test('shows the marker for a charged structured tool surcharge', () => {
assert.equal( expect(
hasToolSurcharge({ hasToolSurcharge({
tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }], tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }],
}), })
true ).toBe(true)
)
}) })
const legacyCases: Array<{ const legacyCases: Array<{
...@@ -63,7 +61,7 @@ describe('tool surcharge detection', () => { ...@@ -63,7 +61,7 @@ describe('tool surcharge detection', () => {
for (const scenario of legacyCases) { for (const scenario of legacyCases) {
test(`keeps the marker visible for legacy ${scenario.name} charges`, () => { 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', () => { ...@@ -93,7 +91,7 @@ describe('tool surcharge detection', () => {
] ]
for (const other of invalidCases) { 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/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { PAYMENT_TYPES } from '../constants' import { PAYMENT_TYPES } from '../constants'
import { requestPaymentAmount } from './use-payment' import { requestPaymentAmount } from './use-payment'
...@@ -44,7 +43,7 @@ describe('payment amount routing', () => { ...@@ -44,7 +43,7 @@ describe('payment amount routing', () => {
}, },
}) })
assert.equal(amount, 18.75) expect(amount).toBe(18.75)
assert.deepEqual(calls, ['waffo:120']) expect(calls).toEqual(['waffo:120'])
}) })
}) })
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { PAYMENT_TYPES } from '../constants' import { PAYMENT_TYPES } from '../constants'
import { import {
...@@ -29,11 +28,11 @@ import { ...@@ -29,11 +28,11 @@ import {
describe('payment type classification', () => { describe('payment type classification', () => {
test('keeps Waffo and Waffo Pancake on their dedicated flows', () => { test('keeps Waffo and Waffo Pancake on their dedicated flows', () => {
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO), true) expect(isWaffoPayment(PAYMENT_TYPES.WAFFO)).toBe(true)
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE), false) expect(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(false)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE), true) expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(true)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO), false) expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO)).toBe(false)
assert.equal(isStripePayment(PAYMENT_TYPES.STRIPE), true) expect(isStripePayment(PAYMENT_TYPES.STRIPE)).toBe(true)
}) })
}) })
...@@ -60,8 +59,8 @@ describe('payment dispatch', () => { ...@@ -60,8 +59,8 @@ describe('payment dispatch', () => {
} }
) )
assert.equal(success, true) expect(success).toBe(true)
assert.deepEqual(calls, ['waffo:120:3']) expect(calls).toEqual(['waffo:120:3'])
}) })
test('does not create a Waffo order without a selected method index', async () => { test('does not create a Waffo order without a selected method index', async () => {
...@@ -80,7 +79,7 @@ describe('payment dispatch', () => { ...@@ -80,7 +79,7 @@ describe('payment dispatch', () => {
} }
) )
assert.equal(success, false) expect(success).toBe(false)
assert.equal(called, false) expect(called).toBe(false)
}) })
}) })
...@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 { QueryClient } from '@tanstack/react-query'
import { afterEach, describe, expect, test } from 'vitest'
import { useAuthStore, type AuthBundle } from '../stores/auth-store' import { useAuthStore, type AuthBundle } from '../stores/auth-store'
import { import {
...@@ -59,10 +57,10 @@ afterEach(() => { ...@@ -59,10 +57,10 @@ afterEach(() => {
describe('authentication session coordination', () => { describe('authentication session coordination', () => {
test('bootstrap distinguishes a completed anonymous check from an active session', async () => { test('bootstrap distinguishes a completed anonymous check from an active session', async () => {
useAuthStore.getState().auth.reset('complete') useAuthStore.getState().auth.reset('complete')
assert.deepEqual(await bootstrapAuthentication(), { kind: 'anonymous' }) expect(await bootstrapAuthentication()).toEqual({ kind: 'anonymous' })
useAuthStore.getState().auth.setBundle(bundle) useAuthStore.getState().auth.setBundle(bundle)
assert.deepEqual(await bootstrapAuthentication(), { expect(await bootstrapAuthentication()).toEqual({
kind: 'authenticated', kind: 'authenticated',
bundle, bundle,
}) })
...@@ -97,10 +95,10 @@ describe('authentication session coordination', () => { ...@@ -97,10 +95,10 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'authenticated') expect(outcome.kind).toBe('authenticated')
assert.deepEqual(requestedSIDs, [bundle.session.sid, undefined]) expect(requestedSIDs).toEqual([bundle.session.sid, undefined])
assert.deepEqual(clears, [[false, 'idle']]) expect(clears).toEqual([[false, 'idle']])
assert.deepEqual(accepted, [bundle]) expect(accepted).toEqual([bundle])
}) })
test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => { test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => {
...@@ -117,10 +115,10 @@ describe('authentication session coordination', () => { ...@@ -117,10 +115,10 @@ describe('authentication session coordination', () => {
wait: async () => undefined, wait: async () => undefined,
} }
assert.deepEqual(await createRefreshRunner(runtime)(), { expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'anonymous', kind: 'anonymous',
}) })
assert.deepEqual(clears, [[true, undefined]]) expect(clears).toEqual([[true, undefined]])
}) })
test('a temporary refresh failure remains retryable without clearing the session', async () => { test('a temporary refresh failure remains retryable without clearing the session', async () => {
...@@ -142,9 +140,9 @@ describe('authentication session coordination', () => { ...@@ -142,9 +140,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(clearCount, 0) expect(clearCount).toBe(0)
assert.equal(transientCount, 1) expect(transientCount).toBe(1)
}) })
test('a rate limited refresh remains retryable without clearing the session', async () => { test('a rate limited refresh remains retryable without clearing the session', async () => {
...@@ -166,9 +164,9 @@ describe('authentication session coordination', () => { ...@@ -166,9 +164,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(clearCount, 0) expect(clearCount).toBe(0)
assert.equal(transientCount, 1) expect(transientCount).toBe(1)
}) })
test('an exhausted refresh race clears the unusable local session', async () => { test('an exhausted refresh race clears the unusable local session', async () => {
...@@ -191,12 +189,12 @@ describe('authentication session coordination', () => { ...@@ -191,12 +189,12 @@ describe('authentication session coordination', () => {
}, },
} }
assert.deepEqual(await createRefreshRunner(runtime)(), { expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync', kind: 'out_of_sync',
code: 'AUTH_REFRESH_RACE', code: 'AUTH_REFRESH_RACE',
}) })
assert.deepEqual(requestedDelays, [80, 200, 500]) expect(requestedDelays).toEqual([80, 200, 500])
assert.deepEqual(clears, [[false, undefined]]) expect(clears).toEqual([[false, undefined]])
}) })
test('an unexpected successful response is treated as out of sync', async () => { test('an unexpected successful response is treated as out of sync', async () => {
...@@ -213,11 +211,11 @@ describe('authentication session coordination', () => { ...@@ -213,11 +211,11 @@ describe('authentication session coordination', () => {
wait: async () => undefined, wait: async () => undefined,
} }
assert.deepEqual(await createRefreshRunner(runtime)(), { expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync', kind: 'out_of_sync',
code: 'AUTH_INVALID_REFRESH_RESPONSE', 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 () => { test('a refresh response cannot restore credentials after a newer auth operation', async () => {
...@@ -241,8 +239,8 @@ describe('authentication session coordination', () => { ...@@ -241,8 +239,8 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(accepted, false) expect(accepted).toBe(false)
}) })
test('explicit rotations update only the current session', () => { test('explicit rotations update only the current session', () => {
...@@ -254,41 +252,35 @@ describe('authentication session coordination', () => { ...@@ -254,41 +252,35 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, last_active_at: 200 }, session: { ...bundle.session, last_active_at: 200 },
}) })
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token') expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
assert.strictEqual(useAuthStore.getState().auth.user, bundle.user) expect(useAuthStore.getState().auth.user).toBe(bundle.user)
assert.throws( expect(() =>
() => applyAuthRotation({
applyAuthRotation({ access_token: 'non-bearer-token',
access_token: 'non-bearer-token', token_type: 'Custom',
token_type: 'Custom', access_expires_at: bundle.access_expires_at + 120,
access_expires_at: bundle.access_expires_at + 120, session: bundle.session,
session: bundle.session, })
}), ).toThrow(/Invalid authentication rotation response/)
/Invalid authentication rotation response/ expect(() =>
) applyAuthRotation({
assert.throws( access_token: 'non-current-token',
() => token_type: 'Bearer',
applyAuthRotation({ access_expires_at: bundle.access_expires_at + 120,
access_token: 'non-current-token', session: { ...bundle.session, current: false },
token_type: 'Bearer', })
access_expires_at: bundle.access_expires_at + 120, ).toThrow(/Invalid authentication rotation response/)
session: { ...bundle.session, current: false },
}), expect(() =>
/Invalid authentication rotation response/ applyAuthRotation({
) access_token: 'wrong-session-token',
token_type: 'Bearer',
assert.throws( access_expires_at: bundle.access_expires_at + 120,
() => session: { ...bundle.session, sid: 'session-b' },
applyAuthRotation({ })
access_token: 'wrong-session-token', ).toThrow(/session mismatch/)
token_type: 'Bearer', expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
access_expires_at: bundle.access_expires_at + 120,
session: { ...bundle.session, sid: 'session-b' },
}),
/session mismatch/
)
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
}) })
test('sign-out clears user-scoped query, mutation, and authentication state', () => { test('sign-out clears user-scoped query, mutation, and authentication state', () => {
...@@ -305,13 +297,13 @@ describe('authentication session coordination', () => { ...@@ -305,13 +297,13 @@ describe('authentication session coordination', () => {
clearAuthenticatedClientState(queryClient, false) clearAuthenticatedClientState(queryClient, false)
assert.equal(queryClient.getQueryCache().getAll().length, 0) expect(queryClient.getQueryCache().getAll().length).toBe(0)
assert.equal(queryClient.getMutationCache().getAll().length, 0) expect(queryClient.getMutationCache().getAll().length).toBe(0)
assert.equal(useAuthStore.getState().auth.user, null) expect(useAuthStore.getState().auth.user).toBe(null)
assert.equal(useAuthStore.getState().auth.accessToken, null) expect(useAuthStore.getState().auth.accessToken).toBe(null)
assert.equal(useAuthStore.getState().auth.session, null) expect(useAuthStore.getState().auth.session).toBe(null)
assert.equal(useAuthStore.getState().auth.pending2FAFlowToken, null) expect(useAuthStore.getState().auth.pending2FAFlowToken).toBe(null)
assert.equal(useAuthStore.getState().auth.bootstrapState, 'complete') expect(useAuthStore.getState().auth.bootstrapState).toBe('complete')
const nextBundle: AuthBundle = { const nextBundle: AuthBundle = {
...bundle, ...bundle,
...@@ -320,8 +312,7 @@ describe('authentication session coordination', () => { ...@@ -320,8 +312,7 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, sid: 'session-b' }, session: { ...bundle.session, sid: 'session-b' },
} }
useAuthStore.getState().auth.setBundle(nextBundle) useAuthStore.getState().auth.setBundle(nextBundle)
assert.equal( expect(queryClient.getQueryData(['account', bundle.user.id])).toBe(
queryClient.getQueryData(['account', bundle.user.id]),
undefined undefined
) )
}) })
......
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { resolveLegacyRoute } from './legacy-route' import { resolveLegacyRoute } from './legacy-route'
...@@ -43,17 +42,15 @@ describe('legacy frontend route migration', () => { ...@@ -43,17 +42,15 @@ describe('legacy frontend route migration', () => {
} }
for (const [source, target] of Object.entries(routes)) { 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', () => { test('preserves search and hash while applying route-specific behavior', () => {
assert.equal( expect(resolveLegacyRoute('/login?redirect=%2Fkeys#continue')).toBe(
resolveLegacyRoute('/login?redirect=%2Fkeys#continue'),
'/sign-in?redirect=%2Fkeys#continue' '/sign-in?redirect=%2Fkeys#continue'
) )
assert.equal( expect(resolveLegacyRoute('/console/topup?source=email#orders')).toBe(
resolveLegacyRoute('/console/topup?source=email#orders'),
'/wallet?source=email#orders' '/wallet?source=email#orders'
) )
}) })
...@@ -75,23 +72,20 @@ describe('legacy frontend route migration', () => { ...@@ -75,23 +72,20 @@ describe('legacy frontend route migration', () => {
} }
for (const [tab, target] of Object.entries(settingsTabs)) { for (const [tab, target] of Object.entries(settingsTabs)) {
assert.equal( expect(
resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`), resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`)
`${target}?tab=${tab}&from=bookmark#form` ).toBe(`${target}?tab=${tab}&from=bookmark#form`)
)
} }
assert.equal( expect(resolveLegacyRoute('/console/setting?tab=unknown')).toBe(
resolveLegacyRoute('/console/setting?tab=unknown'),
'/system-settings?tab=unknown' '/system-settings?tab=unknown'
) )
}) })
test('safely redirects unknown console locations without touching new routes', () => { test('safely redirects unknown console locations without touching new routes', () => {
assert.equal( expect(resolveLegacyRoute('/console/removed?page=2#old')).toBe(
resolveLegacyRoute('/console/removed?page=2#old'),
'/dashboard?page=2#old' '/dashboard?page=2#old'
) )
assert.equal(resolveLegacyRoute('/dashboard'), null) expect(resolveLegacyRoute('/dashboard')).toBe(null)
assert.equal(resolveLegacyRoute('/api/status'), null) expect(resolveLegacyRoute('/api/status')).toBe(null)
}) })
}) })
...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { getServerErrorMessageKey } from './server-error-message' import { getServerErrorMessageKey } from './server-error-message'
...@@ -25,8 +24,8 @@ describe('server error message mapping', () => { ...@@ -25,8 +24,8 @@ describe('server error message mapping', () => {
test('maps the active-session limit to recovery instructions', () => { test('maps the active-session limit to recovery instructions', () => {
const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' }) const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' })
assert.match(message ?? '', /Sign out other sessions/) expect(message ?? '').toMatch(/Sign out other sessions/)
assert.match(message ?? '', /reset your password/) expect(message ?? '').toMatch(/reset your password/)
}) })
test('maps an Axios-shaped issuance limit to rolling-window guidance', () => { test('maps an Axios-shaped issuance limit to rolling-window guidance', () => {
...@@ -34,8 +33,8 @@ describe('server error message mapping', () => { ...@@ -34,8 +33,8 @@ describe('server error message mapping', () => {
response: { data: { code: 'AUTH_SESSION_ISSUANCE_LIMIT' } }, response: { data: { code: 'AUTH_SESSION_ISSUANCE_LIMIT' } },
}) })
assert.match(message ?? '', /rolling window/) expect(message ?? '').toMatch(/rolling window/)
assert.equal(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' }), null) expect(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' })).toBe(null)
}) })
test('maps stable Telegram bind errors without exposing server text', () => { test('maps stable Telegram bind errors without exposing server text', () => {
...@@ -55,16 +54,15 @@ describe('server error message mapping', () => { ...@@ -55,16 +54,15 @@ describe('server error message mapping', () => {
} }
for (const [code, message] of Object.entries(expected)) { for (const [code, message] of Object.entries(expected)) {
assert.equal(getServerErrorMessageKey({ code }), message) expect(getServerErrorMessageKey({ code })).toBe(message)
} }
assert.equal( expect(
getServerErrorMessageKey({ getServerErrorMessageKey({
response: { response: {
data: { code: 'TELEGRAM_BIND_INTERNAL_ERROR', message: 'raw detail' }, 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 @@ ...@@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": 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