test(unit): complete vitest migration for remaining unit suites
This commit is contained in:
parent
22a1b5de57
commit
3307477548
38 changed files with 96 additions and 100 deletions
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
@ -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');
|
||||
|
|
@ -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',
|
||||
|
|
@ -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');
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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<void> {
|
|||
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 };
|
||||
|
|
@ -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');
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
// `<html>` Element (NOT a Document). Reading `loaded.body` directly was
|
||||
// returning undefined for every spine item, so spineText came back ''
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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');
|
||||
|
|
@ -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
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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) ');
|
||||
|
|
@ -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');
|
||||
|
||||
|
|
@ -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');
|
||||
|
|
@ -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([
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 };
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
@ -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',
|
||||
|
|
@ -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)',
|
||||
|
|
@ -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 },
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue