diff --git a/tests/unit/admin-providers-validation.spec.ts b/tests/unit/admin-providers-validation.vitest.spec.ts similarity index 97% rename from tests/unit/admin-providers-validation.spec.ts rename to tests/unit/admin-providers-validation.vitest.spec.ts index ca3b68b..e977cc7 100644 --- a/tests/unit/admin-providers-validation.spec.ts +++ b/tests/unit/admin-providers-validation.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { inArray } from 'drizzle-orm'; import { db } from '../../src/db'; import { adminProviders } from '../../src/db/schema'; @@ -12,7 +12,7 @@ import { type AdminProviderRecord, } from '../../src/lib/server/admin/providers'; -test.describe('admin provider validation', () => { +describe('admin provider validation', () => { test('normalizes valid slugs to lowercase', () => { expect(validateSlug('My-Provider-1')).toBe('my-provider-1'); }); diff --git a/tests/unit/admin-tts-instructions.spec.ts b/tests/unit/admin-tts-instructions.vitest.spec.ts similarity index 92% rename from tests/unit/admin-tts-instructions.spec.ts rename to tests/unit/admin-tts-instructions.vitest.spec.ts index 0a7198e..11e06e8 100644 --- a/tests/unit/admin-tts-instructions.spec.ts +++ b/tests/unit/admin-tts-instructions.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { resolveEffectiveTtsInstructions } from '../../src/lib/server/admin/tts-instructions'; -test.describe('resolveEffectiveTtsInstructions', () => { +describe('resolveEffectiveTtsInstructions', () => { test('uses explicit request instructions when model supports them', () => { const out = resolveEffectiveTtsInstructions({ model: 'gpt-4o-mini-tts', diff --git a/tests/unit/audiobook-scope.spec.ts b/tests/unit/audiobook-scope.vitest.spec.ts similarity index 93% rename from tests/unit/audiobook-scope.spec.ts rename to tests/unit/audiobook-scope.vitest.spec.ts index b73ba40..ca9705b 100644 --- a/tests/unit/audiobook-scope.spec.ts +++ b/tests/unit/audiobook-scope.vitest.spec.ts @@ -1,7 +1,7 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '../../src/lib/server/audiobooks/user-scope'; -test.describe('audiobook scope selection', () => { +describe('audiobook scope selection', () => { test('uses only unclaimed scope when auth is disabled', () => { const result = buildAllowedAudiobookUserIds(false, null, 'unclaimed::ns'); expect(result.preferredUserId).toBe('unclaimed::ns'); diff --git a/tests/unit/audiobook-settings.spec.ts b/tests/unit/audiobook-settings.vitest.spec.ts similarity index 96% rename from tests/unit/audiobook-settings.spec.ts rename to tests/unit/audiobook-settings.vitest.spec.ts index af3c788..1cc371c 100644 --- a/tests/unit/audiobook-settings.spec.ts +++ b/tests/unit/audiobook-settings.vitest.spec.ts @@ -1,10 +1,10 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { canonicalizeAudiobookSettingsForRuntime, coerceAudiobookGenerationSettings, } from '../../src/lib/server/audiobooks/settings'; -test.describe('coerceAudiobookGenerationSettings', () => { +describe('coerceAudiobookGenerationSettings', () => { test('accepts current metadata shape without migration', () => { const result = coerceAudiobookGenerationSettings({ providerRef: 'openai', diff --git a/tests/unit/audiobook.spec.ts b/tests/unit/audiobook.vitest.spec.ts similarity index 95% rename from tests/unit/audiobook.spec.ts rename to tests/unit/audiobook.vitest.spec.ts index 53417a2..0533385 100644 --- a/tests/unit/audiobook.spec.ts +++ b/tests/unit/audiobook.vitest.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { escapeFFMetadata, encodeChapterTitleTag, @@ -7,7 +7,7 @@ import { decodeChapterFileName } from '../../src/lib/server/audiobooks/chapters'; -test.describe('escapeFFMetadata', () => { +describe('escapeFFMetadata', () => { test('escapes special characters correctly', () => { const input = 'Title with = ; # and backslash \\'; // Expected: Equal -> \=, Semicolon -> \;, Hash -> \#, Backslash -> \\ @@ -37,7 +37,7 @@ test.describe('escapeFFMetadata', () => { }); }); -test.describe('Title Tags', () => { +describe('Title Tags', () => { test('encodeChapterTitleTag formats correctly', () => { expect(encodeChapterTitleTag(0, 'Intro')).toBe('0001 - Intro'); expect(encodeChapterTitleTag(9, 'Chapter Ten')).toBe('0010 - Chapter Ten'); @@ -63,7 +63,7 @@ test.describe('Title Tags', () => { }); }); -test.describe('Chapter File Names', () => { +describe('Chapter File Names', () => { test('encodeChapterFileName formats correctly', () => { expect(encodeChapterFileName(0, 'Intro', 'mp3')).toBe('0001__Intro.mp3'); expect(encodeChapterFileName(1, 'Part 2', 'm4b')).toBe('0002__Part%202.m4b'); diff --git a/tests/unit/audiobooks-blobstore.spec.ts b/tests/unit/audiobooks-blobstore.vitest.spec.ts similarity index 95% rename from tests/unit/audiobooks-blobstore.spec.ts rename to tests/unit/audiobooks-blobstore.vitest.spec.ts index 198b2bf..5f2c917 100644 --- a/tests/unit/audiobooks-blobstore.spec.ts +++ b/tests/unit/audiobooks-blobstore.vitest.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { audiobookKey, audiobookPrefix, @@ -14,7 +14,7 @@ function configureS3Env() { process.env.S3_PREFIX = 'openreader-test'; } -test.describe('audiobooks-blobstore', () => { +describe('audiobooks-blobstore', () => { test.beforeAll(() => { configureS3Env(); }); diff --git a/tests/unit/changelog-check.spec.ts b/tests/unit/changelog-check.vitest.spec.ts similarity index 95% rename from tests/unit/changelog-check.spec.ts rename to tests/unit/changelog-check.vitest.spec.ts index f95bcbc..19d8642 100644 --- a/tests/unit/changelog-check.spec.ts +++ b/tests/unit/changelog-check.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { scheduleChangelogCheck } from '../../src/lib/client/changelog-check'; @@ -6,7 +6,7 @@ function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -test.describe('changelog check scheduling', () => { +describe('changelog check scheduling', () => { test('cancels throwaway mount and runs exactly once on remount', async () => { const completedRef = { current: null as string | null }; const inFlightRef = { current: null as string | null }; diff --git a/tests/unit/changelog.spec.ts b/tests/unit/changelog.vitest.spec.ts similarity index 96% rename from tests/unit/changelog.spec.ts rename to tests/unit/changelog.vitest.spec.ts index 02117d9..3d56744 100644 --- a/tests/unit/changelog.spec.ts +++ b/tests/unit/changelog.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { findCurrentVersionIndex, @@ -10,7 +10,7 @@ import { type ChangelogManifestEntry, } from '../../src/lib/shared/changelog'; -test.describe('changelog utilities', () => { +describe('changelog utilities', () => { test('normalizes version strings with and without v prefix', () => { expect(normalizeVersion('v2.2.0')).toBe('2.2.0'); expect(normalizeVersion('2.2.0')).toBe('2.2.0'); diff --git a/tests/unit/document-previews-render.spec.ts b/tests/unit/document-previews-render.vitest.spec.ts similarity index 90% rename from tests/unit/document-previews-render.spec.ts rename to tests/unit/document-previews-render.vitest.spec.ts index 85f6a31..c82ed75 100644 --- a/tests/unit/document-previews-render.spec.ts +++ b/tests/unit/document-previews-render.vitest.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import path from 'path'; import { readFile } from 'fs/promises'; import { @@ -6,7 +6,7 @@ import { renderPdfFirstPageToJpeg, } from '../../src/lib/server/documents/previews-render'; -test.describe('document-previews-render', () => { +describe('document-previews-render', () => { test('renders first PDF page to JPEG preview', async () => { const pdfPath = path.join(process.cwd(), 'tests/files/sample.pdf'); const bytes = await readFile(pdfPath); diff --git a/tests/unit/epub-audiobook-hook.spec.ts b/tests/unit/epub-audiobook-hook.vitest.spec.ts similarity index 94% rename from tests/unit/epub-audiobook-hook.spec.ts rename to tests/unit/epub-audiobook-hook.vitest.spec.ts index 8efd21b..309eb4e 100644 --- a/tests/unit/epub-audiobook-hook.spec.ts +++ b/tests/unit/epub-audiobook-hook.vitest.spec.ts @@ -1,11 +1,11 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { filterNonEmptySpineTextEntries, resolveLoadedSpineSectionDocument, } from '../../src/hooks/epub/useEPUBAudiobook'; -test.describe('EPUB audiobook hook helpers', () => { +describe('EPUB audiobook hook helpers', () => { test('resolveLoadedSpineSectionDocument prefers ownerDocument when available', () => { const ownerDocument = { body: { textContent: 'from ownerDocument' } } as unknown as Document; const loaded = { ownerDocument } as unknown; diff --git a/tests/unit/epub-location-controller.spec.ts b/tests/unit/epub-location-controller.vitest.spec.ts similarity index 92% rename from tests/unit/epub-location-controller.spec.ts rename to tests/unit/epub-location-controller.vitest.spec.ts index ca165a7..c784eab 100644 --- a/tests/unit/epub-location-controller.spec.ts +++ b/tests/unit/epub-location-controller.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { isDirectionalEpubLocation, @@ -6,7 +6,7 @@ import { shouldPersistEpubLocation, } from '../../src/hooks/epub/useEPUBLocationController'; -test.describe('EPUB location controller helpers', () => { +describe('EPUB location controller helpers', () => { test('detects directional locations', () => { expect(isDirectionalEpubLocation('next')).toBe(true); expect(isDirectionalEpubLocation('prev')).toBe(true); diff --git a/tests/unit/epub-spine-coordinates.spec.ts b/tests/unit/epub-spine-coordinates.vitest.spec.ts similarity index 97% rename from tests/unit/epub-spine-coordinates.spec.ts rename to tests/unit/epub-spine-coordinates.vitest.spec.ts index 2d8b9a5..eca51e3 100644 --- a/tests/unit/epub-spine-coordinates.spec.ts +++ b/tests/unit/epub-spine-coordinates.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import type { Book } from 'epubjs'; import { buildEpubLocator, @@ -71,7 +71,7 @@ function makeFakeBook(items: Array<{ index: number; href: string; cfiBase: strin return { book, sections }; } -test.describe('findSegmentOffset', () => { +describe('findSegmentOffset', () => { test('finds an offset for matching text', () => { const spineText = 'Hello world. This is a test paragraph for offsets.'; const offset = findSegmentOffset(spineText, 'this is a test'); @@ -98,7 +98,7 @@ test.describe('findSegmentOffset', () => { }); }); -test.describe('resolveSpineFromCfi', () => { +describe('resolveSpineFromCfi', () => { test('returns spine identity for a CFI inside a known spine', () => { const { book } = makeFakeBook([ { index: 2, href: 'OEBPS/ch02.xhtml', cfiBase: '/6/4', text: 'chapter 2 contents' }, @@ -125,7 +125,7 @@ test.describe('resolveSpineFromCfi', () => { }); }); -test.describe('buildEpubLocator', () => { +describe('buildEpubLocator', () => { test('produces a stable locator with charOffset within the spine item', async () => { const { book } = makeFakeBook([ { @@ -175,7 +175,7 @@ test.describe('buildEpubLocator', () => { }); }); -test.describe('buildEpubLocatorFromChunk', () => { +describe('buildEpubLocatorFromChunk', () => { test('uses the chunk anchor offset as a hint to disambiguate repeated text', () => { const spineText = 'foo bar foo bar foo bar'; const anchorEarly = { @@ -230,7 +230,7 @@ test.describe('buildEpubLocatorFromChunk', () => { }); }); -test.describe('buildEpubLocator anchored-search regression', () => { +describe('buildEpubLocator anchored-search regression', () => { // Pins the contract for the server-side resolver path: when called with // a `chunkText` argument representing the current rendered page, the // returned locator's `charOffset` is **at or after** the chunk's position @@ -261,7 +261,7 @@ test.describe('buildEpubLocator anchored-search regression', () => { }); }); -test.describe('findSegmentOffset fallback contract', () => { +describe('findSegmentOffset fallback contract', () => { // These tests pin the documented behavior of `findSegmentOffset`. The // from-start fallback is correct for single-shot lookups but **wrong** in // a monotonic per-sentence walk — `resolveMonotonicSentenceOffsets` exists @@ -278,7 +278,7 @@ test.describe('findSegmentOffset fallback contract', () => { }); }); -test.describe('resolveMonotonicSentenceOffsets', () => { +describe('resolveMonotonicSentenceOffsets', () => { test('returns monotonically non-decreasing offsets for sentences in order', () => { const spineText = 'The cat sat on the mat. The dog barked loudly. Then it was quiet.'; const sentences = [ @@ -347,7 +347,7 @@ test.describe('resolveMonotonicSentenceOffsets', () => { }); }); -test.describe('getSpineItemPlainText shape handling', () => { +describe('getSpineItemPlainText shape handling', () => { // Regression: epubjs's `Section.load()` resolves to the spine item's // `` Element (NOT a Document). Reading `loaded.body` directly was // returning undefined for every spine item, so spineText came back '' diff --git a/tests/unit/epub-word-highlight.spec.ts b/tests/unit/epub-word-highlight.vitest.spec.ts similarity index 95% rename from tests/unit/epub-word-highlight.spec.ts rename to tests/unit/epub-word-highlight.vitest.spec.ts index dc9720a..8899dc8 100644 --- a/tests/unit/epub-word-highlight.spec.ts +++ b/tests/unit/epub-word-highlight.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildMonotonicWordToTokenMap, @@ -27,7 +27,7 @@ const alignmentWords = (words: string[]): TTSSentenceAlignment['words'] => charEnd: word.length, })); -test.describe('EPUB word highlight mapping', () => { +describe('EPUB word highlight mapping', () => { test('tokenizes canonical segment words with source offsets', () => { const tokens = tokenizeCanonicalSegment(segment('"Hello," she said.', 12)); diff --git a/tests/unit/icons-grid.spec.ts b/tests/unit/icons-grid.vitest.spec.ts similarity index 91% rename from tests/unit/icons-grid.spec.ts rename to tests/unit/icons-grid.vitest.spec.ts index 5663c47..c21f95b 100644 --- a/tests/unit/icons-grid.spec.ts +++ b/tests/unit/icons-grid.vitest.spec.ts @@ -1,7 +1,7 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { iconsGridStyle, maxColumnsForIconGrid } from '../../src/components/doclist/views/iconsGrid'; -test.describe('icons grid layout', () => { +describe('icons grid layout', () => { test('calculates max columns from width and icon size', () => { expect(maxColumnsForIconGrid('md', 136)).toBe(1); expect(maxColumnsForIconGrid('md', 300)).toBe(2); diff --git a/tests/unit/kokoro.spec.ts b/tests/unit/kokoro.vitest.spec.ts similarity index 89% rename from tests/unit/kokoro.spec.ts rename to tests/unit/kokoro.vitest.spec.ts index 3860c1f..5f1950a 100644 --- a/tests/unit/kokoro.spec.ts +++ b/tests/unit/kokoro.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { getMaxVoicesForProvider } from '../../src/lib/shared/kokoro'; -test.describe('kokoro voice limits', () => { +describe('kokoro voice limits', () => { test('keeps Replicate single-voice even for Kokoro models', () => { expect(getMaxVoicesForProvider('replicate', 'kokoro')).toBe(1); expect(getMaxVoicesForProvider('replicate', 'hexgrad/Kokoro-82M')).toBe(1); diff --git a/tests/unit/nlp.spec.ts b/tests/unit/nlp.vitest.spec.ts similarity index 96% rename from tests/unit/nlp.spec.ts rename to tests/unit/nlp.vitest.spec.ts index ce65ef0..4753b20 100644 --- a/tests/unit/nlp.spec.ts +++ b/tests/unit/nlp.vitest.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { preprocessSentenceForAudio, splitTextToTtsBlocks, @@ -18,7 +18,7 @@ const expectNormalizedBlocks = (blocks: string[], maxLen = Number.POSITIVE_INFIN } }; -test.describe('preprocessSentenceForAudio', () => { +describe('preprocessSentenceForAudio', () => { test('normalizes common extraction artifacts', () => { const cases: Array<{ input: string; expected: string }> = [ { @@ -45,7 +45,7 @@ test.describe('preprocessSentenceForAudio', () => { }); }); -test.describe('splitTextToTtsBlocks (PDF-oriented)', () => { +describe('splitTextToTtsBlocks (PDF-oriented)', () => { test('returns [] for empty input', () => { expect(splitTextToTtsBlocks('')).toEqual([]); expect(splitTextToTtsBlocks(' ')).toEqual([]); @@ -160,7 +160,7 @@ test.describe('splitTextToTtsBlocks (PDF-oriented)', () => { }); }); -test.describe('splitTextToTtsBlocksEPUB (highlight-friendly)', () => { +describe('splitTextToTtsBlocksEPUB (highlight-friendly)', () => { test('returns [] for empty input', () => { expect(splitTextToTtsBlocksEPUB('')).toEqual([]); expect(splitTextToTtsBlocksEPUB(' ')).toEqual([]); @@ -193,7 +193,7 @@ test.describe('splitTextToTtsBlocksEPUB (highlight-friendly)', () => { }); }); -test.describe('normalizeTextForTts', () => { +describe('normalizeTextForTts', () => { test('returns a single normalized string without newlines', () => { const input = 'Hello.\nWorld.\n\nNext paragraph.'; const normalized = normalizeTextForTts(input); diff --git a/tests/unit/onboarding-flow.spec.ts b/tests/unit/onboarding-flow.vitest.spec.ts similarity index 94% rename from tests/unit/onboarding-flow.spec.ts rename to tests/unit/onboarding-flow.vitest.spec.ts index 9533aa8..27eaaa6 100644 --- a/tests/unit/onboarding-flow.spec.ts +++ b/tests/unit/onboarding-flow.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { createCoalescedAsyncRunner, resolveNextOnboardingStep } from '../../src/lib/client/onboarding-flow'; -test.describe('onboarding flow resolver', () => { +describe('onboarding flow resolver', () => { test('resolves deterministic order with privacy first', () => { const step = resolveNextOnboardingStep({ privacyRequired: true, @@ -78,7 +78,7 @@ test.describe('onboarding flow resolver', () => { }); }); -test.describe('coalesced onboarding runner', () => { +describe('coalesced onboarding runner', () => { test('coalesces concurrent triggers into one extra rerun', async () => { let runs = 0; let nestedRequested = false; diff --git a/tests/unit/onboarding-state.spec.ts b/tests/unit/onboarding-state.vitest.spec.ts similarity index 90% rename from tests/unit/onboarding-state.spec.ts rename to tests/unit/onboarding-state.vitest.spec.ts index 9379c2a..c457b92 100644 --- a/tests/unit/onboarding-state.spec.ts +++ b/tests/unit/onboarding-state.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { ONBOARDING_STATE_REGISTRY } from '../../src/lib/shared/onboarding-state'; import { SYNCED_PREFERENCE_KEYS } from '../../src/types/user-state'; -test.describe('onboarding state storage scopes', () => { +describe('onboarding state storage scopes', () => { test('keeps local onboarding flags out of synced server preferences', () => { expect(ONBOARDING_STATE_REGISTRY.privacyAccepted.scope).toBe('local-dexie'); expect(ONBOARDING_STATE_REGISTRY.firstVisitSettingsOpened.scope).toBe('local-dexie'); diff --git a/tests/unit/sha256.spec.ts b/tests/unit/sha256.vitest.spec.ts similarity index 91% rename from tests/unit/sha256.spec.ts rename to tests/unit/sha256.vitest.spec.ts index 64796a3..1a44fec 100644 --- a/tests/unit/sha256.spec.ts +++ b/tests/unit/sha256.vitest.spec.ts @@ -1,10 +1,10 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { sha256HexSoftwareFromBytes, sha256HexFromString } from '../../src/lib/client/sha256'; -test.describe('SHA256 Software Implementation', () => { +describe('SHA256 Software Implementation', () => { // Known test vectors // "" -> e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 // "abc" -> ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad diff --git a/tests/unit/transfer-user-documents.spec.ts b/tests/unit/transfer-user-documents.vitest.spec.ts similarity index 95% rename from tests/unit/transfer-user-documents.spec.ts rename to tests/unit/transfer-user-documents.vitest.spec.ts index ca58e28..f1c0bed 100644 --- a/tests/unit/transfer-user-documents.spec.ts +++ b/tests/unit/transfer-user-documents.vitest.spec.ts @@ -1,11 +1,11 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; import { eq } from 'drizzle-orm'; import { documents } from '../../src/db/schema_sqlite'; import { transferUserDocuments } from '../../src/lib/server/user/claim-data'; -test.describe('transferUserDocuments', () => { +describe('transferUserDocuments', () => { test('moves document rows to new user without PK conflicts', async () => { process.env.BASE_URL = 'http://localhost:3003'; process.env.AUTH_SECRET = 'test-secret'; diff --git a/tests/unit/tts-audio-warm-cache.spec.ts b/tests/unit/tts-audio-warm-cache.vitest.spec.ts similarity index 97% rename from tests/unit/tts-audio-warm-cache.spec.ts rename to tests/unit/tts-audio-warm-cache.vitest.spec.ts index 06a5d24..4f38c73 100644 --- a/tests/unit/tts-audio-warm-cache.spec.ts +++ b/tests/unit/tts-audio-warm-cache.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { releaseWarmAudio, upsertWarmAudioEntry, @@ -23,7 +23,7 @@ function createTrackedAudio() { return { audio, calls }; } -test.describe('tts warm audio cache helpers', () => { +describe('tts warm audio cache helpers', () => { test('releaseWarmAudio clears source and invokes teardown methods', () => { const tracked = createTrackedAudio(); releaseWarmAudio(tracked.audio); diff --git a/tests/unit/tts-epub-handoff.spec.ts b/tests/unit/tts-epub-handoff.vitest.spec.ts similarity index 97% rename from tests/unit/tts-epub-handoff.spec.ts rename to tests/unit/tts-epub-handoff.vitest.spec.ts index 143e6e1..fa5fb82 100644 --- a/tests/unit/tts-epub-handoff.spec.ts +++ b/tests/unit/tts-epub-handoff.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { completedEpubBoundarySegment, @@ -23,7 +23,7 @@ const segment = ( ...overrides, }); -test.describe('EPUB boundary handoff', () => { +describe('EPUB boundary handoff', () => { test('records only completed segments that span a source boundary', () => { expect(completedEpubBoundarySegment(segment('same page'))).toBeNull(); diff --git a/tests/unit/tts-epub-preload.spec.ts b/tests/unit/tts-epub-preload.vitest.spec.ts similarity index 94% rename from tests/unit/tts-epub-preload.spec.ts rename to tests/unit/tts-epub-preload.vitest.spec.ts index d780e51..f74dd39 100644 --- a/tests/unit/tts-epub-preload.spec.ts +++ b/tests/unit/tts-epub-preload.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import type { CanonicalTtsSourceUnit } from '../../src/lib/shared/tts-segment-plan'; import { @@ -6,7 +6,7 @@ import { selectUpcomingWalkerItems, } from '../../src/lib/client/epub/tts-epub-preload'; -test.describe('EPUB walker preload helpers', () => { +describe('EPUB walker preload helpers', () => { test('selectUpcomingWalkerItems honors depth as current + (depth-1) upcoming', () => { const items = [ { cfi: 'epubcfi(/6/2[;s=a]!/4/2)' }, // same as current after normalization diff --git a/tests/unit/tts-generate.spec.ts b/tests/unit/tts-generate.vitest.spec.ts similarity index 92% rename from tests/unit/tts-generate.spec.ts rename to tests/unit/tts-generate.vitest.spec.ts index 364f6f2..a601952 100644 --- a/tests/unit/tts-generate.spec.ts +++ b/tests/unit/tts-generate.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { extractReplicateAudioUrl } from '../../src/lib/server/tts/generate'; -test.describe('replicate output URL extraction', () => { +describe('replicate output URL extraction', () => { test('returns direct URL string output', () => { expect(extractReplicateAudioUrl('https://replicate.delivery/audio.mp3')).toBe( 'https://replicate.delivery/audio.mp3' diff --git a/tests/unit/tts-locator.spec.ts b/tests/unit/tts-locator.vitest.spec.ts similarity index 96% rename from tests/unit/tts-locator.spec.ts rename to tests/unit/tts-locator.vitest.spec.ts index 19e0eee..6e8b32c 100644 --- a/tests/unit/tts-locator.spec.ts +++ b/tests/unit/tts-locator.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { compareSegmentLocators, locatorGroupKey, @@ -25,7 +25,7 @@ const epubLocator = ( ...(cfi ? { cfi } : {}), }); -test.describe('locatorGroupKey', () => { +describe('locatorGroupKey', () => { test('groups stable EPUB locators by spine index and href', () => { expect(locatorGroupKey(epubLocator('OEBPS/ch02.xhtml', 2, 0))).toBe('epub:2:OEBPS/ch02.xhtml'); // Different charOffset, same spine → same group (sidebar bucket is chapter-sized). @@ -55,7 +55,7 @@ test.describe('locatorGroupKey', () => { }); }); -test.describe('locatorIdentityKey', () => { +describe('locatorIdentityKey', () => { test('two EPUB rows in the same chapter at different charOffsets get distinct identity keys', () => { // This is the bug that caused page-N rows to swallow page-(N+1) rows in // the server-side manifest aggregator: the previous code keyed by the @@ -82,7 +82,7 @@ test.describe('locatorIdentityKey', () => { }); }); -test.describe('compareSegmentLocators', () => { +describe('compareSegmentLocators', () => { test('orders EPUB rows by spineIndex then charOffset numerically', () => { // Spine 10 must come AFTER spine 2 — a lexicographic compare would // wrongly place "10" before "2". @@ -137,7 +137,7 @@ test.describe('compareSegmentLocators', () => { }); }); -test.describe('type guards', () => { +describe('type guards', () => { test('isStableEpubLocator requires all spine fields', () => { expect(isStableEpubLocator(epubLocator('a.xhtml', 0, 0))).toBe(true); expect(isStableEpubLocator({ readerType: 'epub', spineHref: 'a.xhtml' })).toBe(false); @@ -159,7 +159,7 @@ test.describe('type guards', () => { }); }); -test.describe('normalizeEpubLocationToken', () => { +describe('normalizeEpubLocationToken', () => { test('strips whitespace and step markers but preserves identity', () => { const a = normalizeEpubLocationToken('epubcfi(/6/8!/4:0)'); const b = normalizeEpubLocationToken(' epubcfi(/6/8[;s=a]!/4:0) '); diff --git a/tests/unit/tts-provider-catalog.spec.ts b/tests/unit/tts-provider-catalog.vitest.spec.ts similarity index 99% rename from tests/unit/tts-provider-catalog.spec.ts rename to tests/unit/tts-provider-catalog.vitest.spec.ts index 3c40372..334ea16 100644 --- a/tests/unit/tts-provider-catalog.spec.ts +++ b/tests/unit/tts-provider-catalog.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { REPLICATE_KOKORO_82M_VERSIONED_MODEL, @@ -13,7 +13,7 @@ import { resolveReplicateVoiceInputKey, resolveVoices } from '../../src/lib/serv import { applyConfigUpdate, getVoicePreferenceKey } from '../../src/lib/client/config/updates'; import { buildSyncedPreferencePatch } from '../../src/lib/client/config/preferences'; -test.describe('tts provider catalog', () => { +describe('tts provider catalog', () => { test('resolves provider models with fixed Deepinfra catalog', () => { expect(resolveProviderModels('openai').map((model) => model.id)).toEqual([ 'tts-1', @@ -389,7 +389,7 @@ test.describe('tts provider catalog', () => { }); }); -test.describe('config helpers', () => { +describe('config helpers', () => { test('persists voice preferences by provider/model pair', () => { expect(getVoicePreferenceKey('openai', 'tts-1')).toBe('openai:tts-1'); diff --git a/tests/unit/tts-segment-audio-urls.spec.ts b/tests/unit/tts-segment-audio-urls.vitest.spec.ts similarity index 94% rename from tests/unit/tts-segment-audio-urls.spec.ts rename to tests/unit/tts-segment-audio-urls.vitest.spec.ts index 93b8d92..742d53a 100644 --- a/tests/unit/tts-segment-audio-urls.spec.ts +++ b/tests/unit/tts-segment-audio-urls.vitest.spec.ts @@ -1,10 +1,10 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildSegmentAudioFallbackUrl, resolveSegmentAudioUrls, } from '../../src/lib/server/tts/segment-audio-urls'; -test.describe('tts segment audio url resolution', () => { +describe('tts segment audio url resolution', () => { test('builds fallback URL with encoded params', () => { const url = buildSegmentAudioFallbackUrl('doc 1', 'segment/2'); expect(url).toBe('/api/tts/segments/audio/fallback?documentId=doc%201&segmentId=segment%2F2'); diff --git a/tests/unit/tts-segment-plan-leading.spec.ts b/tests/unit/tts-segment-plan-leading.vitest.spec.ts similarity index 98% rename from tests/unit/tts-segment-plan-leading.spec.ts rename to tests/unit/tts-segment-plan-leading.vitest.spec.ts index 689d36a..3114733 100644 --- a/tests/unit/tts-segment-plan-leading.spec.ts +++ b/tests/unit/tts-segment-plan-leading.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { planCanonicalTtsSegments } from '../../src/lib/shared/tts-segment-plan'; -test.describe('planCanonicalTtsSegments – leading context handling', () => { +describe('planCanonicalTtsSegments – leading context handling', () => { test('clean boundary: splits block so current source keeps its first sentence', () => { const currentSourceKey = 'str:epubcfi(/6/10!/4/2)'; const plan = planCanonicalTtsSegments([ diff --git a/tests/unit/tts-segment-plan.spec.ts b/tests/unit/tts-segment-plan.vitest.spec.ts similarity index 98% rename from tests/unit/tts-segment-plan.spec.ts rename to tests/unit/tts-segment-plan.vitest.spec.ts index 8f03797..c9a3740 100644 --- a/tests/unit/tts-segment-plan.spec.ts +++ b/tests/unit/tts-segment-plan.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildSegmentKey, buildSegmentKeyPrefix, planCanonicalTtsSegments } from '../../src/lib/shared/tts-segment-plan'; -test.describe('planCanonicalTtsSegments', () => { +describe('planCanonicalTtsSegments', () => { test('emits a cross-boundary segment once and assigns it to the source where it starts', () => { const plan = planCanonicalTtsSegments([ { @@ -223,7 +223,7 @@ test.describe('planCanonicalTtsSegments', () => { }); }); -test.describe('buildSegmentKeyPrefix / buildSegmentKey contract', () => { +describe('buildSegmentKeyPrefix / buildSegmentKey contract', () => { // These keys are the bridge between persistence and the sidebar's merge of // synth rows with manifest rows. Both sides must produce identical keys for // the same `(documentId, readerType, text)` triple, or the sidebar will diff --git a/tests/unit/tts-segment-retry-policy.spec.ts b/tests/unit/tts-segment-retry-policy.vitest.spec.ts similarity index 93% rename from tests/unit/tts-segment-retry-policy.spec.ts rename to tests/unit/tts-segment-retry-policy.vitest.spec.ts index 0dd6e29..140e518 100644 --- a/tests/unit/tts-segment-retry-policy.spec.ts +++ b/tests/unit/tts-segment-retry-policy.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { isRetryableSegmentStatus, @@ -6,7 +6,7 @@ import { shouldDeferSegmentRetry, } from '../../src/lib/client/tts/segment-retry-policy'; -test.describe('tts segment retry policy', () => { +describe('tts segment retry policy', () => { test('marks pending/error statuses as retryable', () => { expect(isRetryableSegmentStatus('pending')).toBe(true); expect(isRetryableSegmentStatus('error')).toBe(true); diff --git a/tests/unit/tts-segments-audio-cache.spec.ts b/tests/unit/tts-segments-audio-cache.vitest.spec.ts similarity index 91% rename from tests/unit/tts-segments-audio-cache.spec.ts rename to tests/unit/tts-segments-audio-cache.vitest.spec.ts index ce09173..a1dd8d9 100644 --- a/tests/unit/tts-segments-audio-cache.spec.ts +++ b/tests/unit/tts-segments-audio-cache.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildSegmentAudioCacheHeaders, normalizeAudioByteRangeHeader, @@ -6,7 +6,7 @@ import { TTS_SEGMENT_FALLBACK_CACHE_CONTROL, } from '../../src/lib/server/tts/segments-audio'; -test.describe('tts segment audio cache headers', () => { +describe('tts segment audio cache headers', () => { test('builds fallback cache headers', () => { expect(buildSegmentAudioCacheHeaders()).toEqual({ 'Cache-Control': TTS_SEGMENT_FALLBACK_CACHE_CONTROL, diff --git a/tests/unit/tts-segments-manifest.spec.ts b/tests/unit/tts-segments-manifest.vitest.spec.ts similarity index 94% rename from tests/unit/tts-segments-manifest.spec.ts rename to tests/unit/tts-segments-manifest.vitest.spec.ts index 513ca5c..ad02218 100644 --- a/tests/unit/tts-segments-manifest.spec.ts +++ b/tests/unit/tts-segments-manifest.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { compareManifestSegments, decodeManifestCursor, @@ -6,8 +6,9 @@ import { encodeManifestCursor, parseManifestPageSize, } from '../../src/lib/server/tts/segments-manifest'; +import { locatorGroupKey, locatorIdentityKey } from '../../src/lib/shared/tts-locator'; -test.describe('tts segments manifest helpers', () => { +describe('tts segments manifest helpers', () => { test('dedupe prefers completed variant over newer pending for same settings key', () => { const variants = dedupeManifestVariants([ { @@ -125,8 +126,6 @@ test.describe('tts segments manifest helpers', () => { const sorted = rows.sort(compareManifestSegments); // Recompute groupKey using the production helper to assert the actual // grouping behavior rather than the test's hand-labeled groupKey. - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { locatorGroupKey } = require('../../src/lib/shared/tts-locator'); const groupKeys = sorted.map((row) => locatorGroupKey(row.locator)); expect(groupKeys[0]).toBe(groupKeys[1]); // both ch02 expect(groupKeys[2]).not.toBe(groupKeys[0]); // ch03 is its own group @@ -141,9 +140,6 @@ test.describe('tts segments manifest helpers', () => { // rows visually "disappear" when later pages were generated. // // The fix uses `locatorIdentityKey` which includes charOffset for EPUB. - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { locatorIdentityKey, locatorGroupKey } = require('../../src/lib/shared/tts-locator'); - const page1Row0 = { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 0 }; const page2Row0 = { readerType: 'epub' as const, spineHref: 'OEBPS/ch02.xhtml', spineIndex: 2, charOffset: 2048 }; diff --git a/tests/unit/tts-segments.spec.ts b/tests/unit/tts-segments.vitest.spec.ts similarity index 98% rename from tests/unit/tts-segments.spec.ts rename to tests/unit/tts-segments.vitest.spec.ts index ddd2125..def8e9f 100644 --- a/tests/unit/tts-segments.spec.ts +++ b/tests/unit/tts-segments.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildProportionalAlignment, buildTtsSegmentEntryId, @@ -11,7 +11,7 @@ import { projectSegmentLocator, } from '../../src/lib/server/tts/segments'; -test.describe('tts segment helpers', () => { +describe('tts segment helpers', () => { test('builds stable settings hash', () => { const a = buildTtsSegmentSettingsHash({ providerRef: 'openai', diff --git a/tests/unit/tts-settings-admin-view-model.spec.ts b/tests/unit/tts-settings-admin-view-model.vitest.spec.ts similarity index 97% rename from tests/unit/tts-settings-admin-view-model.spec.ts rename to tests/unit/tts-settings-admin-view-model.vitest.spec.ts index c207ce6..8b1f32f 100644 --- a/tests/unit/tts-settings-admin-view-model.spec.ts +++ b/tests/unit/tts-settings-admin-view-model.vitest.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { resolveTtsSettingsViewModel } from '../../src/lib/client/settings/tts-settings'; import type { SharedProviderEntry } from '../../src/hooks/useSharedProviders'; @@ -20,7 +20,7 @@ const SHARED: SharedProviderEntry[] = [ }, ]; -test.describe('resolveTtsSettingsViewModel (admin/shared modes)', () => { +describe('resolveTtsSettingsViewModel (admin/shared modes)', () => { test('keeps default-openai selection when that shared provider exists', () => { const vm = resolveTtsSettingsViewModel({ providerRef: 'default-openai', diff --git a/tests/unit/walker-theme.spec.ts b/tests/unit/walker-theme.vitest.spec.ts similarity index 93% rename from tests/unit/walker-theme.spec.ts rename to tests/unit/walker-theme.vitest.spec.ts index c5f5000..777b963 100644 --- a/tests/unit/walker-theme.spec.ts +++ b/tests/unit/walker-theme.vitest.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildWalkerThemeRules } from '../../src/lib/client/epub/walker-theme'; -test.describe('walker theme rules', () => { +describe('walker theme rules', () => { test('always includes foreground/background colors', () => { const rules = buildWalkerThemeRules({ foreground: 'rgb(1, 2, 3)', diff --git a/tests/unit/whisper-alignment-mapping.spec.ts b/tests/unit/whisper-alignment-mapping.vitest.spec.ts similarity index 88% rename from tests/unit/whisper-alignment-mapping.spec.ts rename to tests/unit/whisper-alignment-mapping.vitest.spec.ts index 5f91973..b53a4f6 100644 --- a/tests/unit/whisper-alignment-mapping.spec.ts +++ b/tests/unit/whisper-alignment-mapping.vitest.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { mapWordsToSentenceOffsets, } from '@openreader/compute-core'; -test.describe('whisper alignment mapping', () => { +describe('whisper alignment mapping', () => { test('maps words to sentence offsets with punctuation and repeated spaces', () => { const aligned = mapWordsToSentenceOffsets('Hello, world again.', [ { word: 'Hello', start: 0, end: 0.25 }, diff --git a/tests/unit/whisper-spectral.spec.ts b/tests/unit/whisper-spectral.vitest.spec.ts similarity index 91% rename from tests/unit/whisper-spectral.spec.ts rename to tests/unit/whisper-spectral.vitest.spec.ts index 90857a2..2b87a04 100644 --- a/tests/unit/whisper-spectral.spec.ts +++ b/tests/unit/whisper-spectral.vitest.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import { buildGoertzelCoefficients, goertzelPower } from '@openreader/compute-core'; function dftPower(samples: Float32Array, k: number): number { @@ -13,7 +13,7 @@ function dftPower(samples: Float32Array, k: number): number { return (re * re) + (im * im); } -test.describe('whisper spectral helpers', () => { +describe('whisper spectral helpers', () => { test('goertzel power matches direct DFT for non-power-of-two frame size', () => { const frameSize = 400; const bins = 201; diff --git a/tests/unit/whisper-token-timestamps.spec.ts b/tests/unit/whisper-token-timestamps.vitest.spec.ts similarity index 95% rename from tests/unit/whisper-token-timestamps.spec.ts rename to tests/unit/whisper-token-timestamps.vitest.spec.ts index a584d6f..c2aded0 100644 --- a/tests/unit/whisper-token-timestamps.spec.ts +++ b/tests/unit/whisper-token-timestamps.vitest.spec.ts @@ -1,11 +1,11 @@ -import { test, expect } from '@playwright/test'; +import { describe, expect, test } from 'vitest'; import * as ort from 'onnxruntime-node'; import { buildWordsFromTimestampedTokens, extractTokenStartTimestamps, } from '@openreader/compute-core'; -test.describe('whisper token timestamp alignment', () => { +describe('whisper token timestamp alignment', () => { test('extracts monotonic token timestamps from cross-attention maps', () => { const seqLen = 6; const frames = 10;