fix(tests): try to get tests working again

This commit is contained in:
Richard R 2026-01-28 01:04:01 -07:00
parent 6089d2c445
commit 20111b1e36
18 changed files with 97 additions and 92 deletions

View file

@ -17,7 +17,7 @@ import type { TTSAudiobookChapter } from '@/types/tts';
import type { AudiobookGenerationSettings } from '@/types/client';
import { resolveDocumentId } from '@/lib/dexie';
import { RateLimitBanner } from '@/components/rate-limit-banner';
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
@ -26,7 +26,7 @@ export default function EPUBPage() {
const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter } = useEPUB();
const { stop } = useTTS();
const { isAtLimit } = useAutoRateLimit();
const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);

View file

@ -14,14 +14,14 @@ import TTSPlayer from '@/components/player/TTSPlayer';
import { resolveDocumentId } from '@/lib/dexie';
import { DocumentHeaderMenu } from '@/components/DocumentHeaderMenu';
import { RateLimitBanner } from '@/components/rate-limit-banner';
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
export default function HTMLPage() {
const { id } = useParams();
const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
const { stop } = useTTS();
const { isAtLimit } = useAutoRateLimit();
const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);

View file

@ -17,7 +17,7 @@ import TTSPlayer from '@/components/player/TTSPlayer';
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
import { resolveDocumentId } from '@/lib/dexie';
import { RateLimitBanner } from '@/components/rate-limit-banner';
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
@ -35,7 +35,7 @@ export default function PDFViewerPage() {
const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc, currDocPage, currDocPages, createFullAudioBook: createPDFAudioBook, regenerateChapter: regeneratePDFChapter } = usePDF();
const { stop } = useTTS();
const { isAtLimit } = useAutoRateLimit();
const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [zoomLevel, setZoomLevel] = useState<number>(100);

View file

@ -7,7 +7,7 @@ import { TTSProvider } from '@/contexts/TTSContext';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { ConfigProvider } from '@/contexts/ConfigContext';
import { HTMLProvider } from '@/contexts/HTMLContext';
import { AutoRateLimitProvider } from '@/contexts/AutoRateLimitContext';
import { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext';
import { PrivacyPopup } from '@/components/privacy-popup';
interface ProvidersProps {
@ -18,7 +18,7 @@ interface ProvidersProps {
export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps) {
return (
<AutoRateLimitProvider authEnabled={authEnabled} authBaseUrl={authBaseUrl}>
<AuthRateLimitProvider authEnabled={authEnabled} authBaseUrl={authBaseUrl}>
<ThemeProvider>
<ConfigProvider>
<DocumentProvider>
@ -35,6 +35,6 @@ export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps
</DocumentProvider>
</ConfigProvider>
</ThemeProvider>
</AutoRateLimitProvider>
</AuthRateLimitProvider>
);
}

View file

@ -5,7 +5,7 @@ import { Button, Input } from '@headlessui/react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { getAuthClient } from '@/lib/auth-client';
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { showPrivacyPopup } from '@/components/privacy-popup';
import { wasSignedOut, clearSignedOut } from '@/lib/session-utils';
import { GithubIcon } from '@/components/icons/Icons';
@ -32,7 +32,7 @@ function SignInContent() {
const [justSignedOut, setJustSignedOut] = useState(false);
const [error, setError] = useState<string | null>(null);
const { authEnabled, baseUrl } = useAuthConfig();
const { refresh: refreshRateLimit } = useAutoRateLimit();
const { refresh: refreshRateLimit } = useAuthRateLimit();
const isAnyLoading = loadingEmail || loadingGithub || loadingGuest;

View file

@ -5,7 +5,7 @@ import { Button, Input } from '@headlessui/react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { getAuthClient } from '@/lib/auth-client';
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { showPrivacyPopup } from '@/components/privacy-popup';
import { LoadingSpinner } from '@/components/Spinner';
import toast from 'react-hot-toast';
@ -19,7 +19,7 @@ export default function SignUpPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { authEnabled, baseUrl } = useAuthConfig();
const { refresh: refreshRateLimit } = useAutoRateLimit();
const { refresh: refreshRateLimit } = useAuthRateLimit();
// Check if auth is enabled, redirect home if not
useEffect(() => {

View file

@ -2,7 +2,7 @@
import { GithubIcon } from '@/components/icons/Icons'
import { showPrivacyPopup } from '@/components/privacy-popup'
import { useAuthConfig } from '@/contexts/AutoRateLimitContext'
import { useAuthConfig } from '@/contexts/AuthRateLimitContext'
export function Footer() {
const { authEnabled } = useAuthConfig();

View file

@ -35,7 +35,7 @@ import { BaseDocument } from '@/types/documents';
import { getAuthClient } from '@/lib/auth-client';
import { useAuthSession } from '@/hooks/useAuth';
import { clearSignedOut } from '@/lib/session-utils';
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useRouter } from 'next/navigation';
import { showPrivacyPopup } from '@/components/privacy-popup';
@ -86,7 +86,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false);
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
const { refresh: refreshRateLimit } = useAutoRateLimit();
const { refresh: refreshRateLimit } = useAuthRateLimit();
const { data: session } = useAuthSession();
const router = useRouter();

View file

@ -1,14 +1,14 @@
'use client';
import { useEffect, useState, ReactNode } from 'react';
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useAuthSession } from '@/hooks/useAuth';
import { getAuthClient } from '@/lib/auth-client';
import { LoadingSpinner } from '@/components/Spinner';
export function AuthLoader({ children }: { children: ReactNode }) {
const { authEnabled, baseUrl } = useAuthConfig();
const { refresh: refreshRateLimit } = useAutoRateLimit();
const { refresh: refreshRateLimit } = useAuthRateLimit();
const { data: session, isPending } = useAuthSession();
const [isAutoLoggingIn, setIsAutoLoggingIn] = useState(false);
const [isCheckingSignOut, setIsCheckingSignOut] = useState(true);

View file

@ -2,7 +2,7 @@
import { Button } from '@headlessui/react';
import Link from 'next/link';
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useAuthSession } from '@/hooks/useAuth';
import { getAuthClient } from '@/lib/auth-client';
import { clearSignedOut } from '@/lib/session-utils';
@ -10,7 +10,7 @@ import { useRouter } from 'next/navigation';
export function UserMenu({ className = '' }: { className?: string }) {
const { authEnabled, baseUrl } = useAuthConfig();
const { refresh: refreshRateLimit } = useAutoRateLimit();
const { refresh: refreshRateLimit } = useAuthRateLimit();
const { data: session, isPending } = useAuthSession();
const router = useRouter();

View file

@ -1,6 +1,6 @@
'use client';
import { useAutoRateLimit, formatCharCount } from '@/contexts/AutoRateLimitContext';
import { useAuthRateLimit, formatCharCount } from '@/contexts/AuthRateLimitContext';
import Link from 'next/link';
interface RateLimitBannerProps {
@ -8,7 +8,7 @@ interface RateLimitBannerProps {
}
export function RateLimitBanner({ className = '' }: RateLimitBannerProps) {
const { status, isAtLimit, timeUntilReset, authEnabled } = useAutoRateLimit();
const { status, isAtLimit, timeUntilReset, authEnabled } = useAuthRateLimit();
// Don't show banner if auth is not enabled or if not at limit
if (!authEnabled || !status?.authEnabled || !isAtLimit) {
@ -49,7 +49,7 @@ export function RateLimitBanner({ className = '' }: RateLimitBannerProps) {
* Compact version for inline display
*/
export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
const { status, isAtLimit, authEnabled } = useAutoRateLimit();
const { status, isAtLimit, authEnabled } = useAuthRateLimit();
// Don't show if auth is not enabled
if (!authEnabled || !status?.authEnabled) {

View file

@ -12,7 +12,7 @@ export interface RateLimitStatus {
authEnabled: boolean;
}
interface AutoRateLimitContextType {
interface AuthRateLimitContextType {
// Auth Config
authEnabled: boolean;
authBaseUrl: string | null;
@ -30,24 +30,24 @@ interface AutoRateLimitContextType {
triggerRateLimit: () => void;
}
const AutoRateLimitContext = createContext<AutoRateLimitContextType | null>(null);
const AuthRateLimitContext = createContext<AuthRateLimitContextType | null>(null);
export function useAutoRateLimit(): AutoRateLimitContextType {
const context = useContext(AutoRateLimitContext);
export function useAuthRateLimit(): AuthRateLimitContextType {
const context = useContext(AuthRateLimitContext);
if (!context) {
throw new Error('useAutoRateLimit must be used within a AutoRateLimitProvider');
throw new Error('useAuthRateLimit must be used within an AuthRateLimitProvider');
}
return context;
}
// Re-export specific hooks for backward compatibility or convenience if needed
export function useAuthConfig() {
const { authEnabled, authBaseUrl } = useAutoRateLimit();
const { authEnabled, authBaseUrl } = useAuthRateLimit();
return { authEnabled, baseUrl: authBaseUrl };
}
export function useRateLimit() {
return useAutoRateLimit();
return useAuthRateLimit();
}
function calculateTimeUntilReset(resetTime: Date): string {
@ -82,13 +82,13 @@ export function formatCharCount(count: number): string {
return count.toString();
}
interface AutoRateLimitProviderProps {
interface AuthRateLimitProviderProps {
children: ReactNode;
authEnabled: boolean;
authBaseUrl: string | null;
}
export function AutoRateLimitProvider({ children, authEnabled, authBaseUrl }: AutoRateLimitProviderProps) {
export function AuthRateLimitProvider({ children, authEnabled, authBaseUrl }: AuthRateLimitProviderProps) {
const [status, setStatus] = useState<RateLimitStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -209,7 +209,7 @@ export function AutoRateLimitProvider({ children, authEnabled, authBaseUrl }: Au
};
}, []);
const contextValue: AutoRateLimitContextType = {
const contextValue: AuthRateLimitContextType = {
authEnabled,
authBaseUrl,
status,
@ -225,8 +225,8 @@ export function AutoRateLimitProvider({ children, authEnabled, authBaseUrl }: Au
};
return (
<AutoRateLimitContext.Provider value={contextValue}>
<AuthRateLimitContext.Provider value={contextValue}>
{children}
</AutoRateLimitContext.Provider>
</AuthRateLimitContext.Provider>
);
}

View file

@ -39,7 +39,7 @@ import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
import { withRetry, generateTTS, alignAudio } from '@/lib/client';
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/nlp';
import { isKokoroModel } from '@/utils/voice';
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import type {
TTSLocation,
TTSSmartMergeResult,
@ -299,7 +299,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
const audioContext = useAudioContext();
const audioCache = useAudioCache(25);
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl, configTTSProvider, configTTSModel);
const { onTTSStart, onTTSComplete, refresh: refreshRateLimit, triggerRateLimit, isAtLimit } = useAutoRateLimit();
const { onTTSStart, onTTSComplete, refresh: refreshRateLimit, triggerRateLimit, isAtLimit } = useAuthRateLimit();
// Add ref for location change handler
const locationChangeHandlerRef = useRef<((location: TTSLocation) => void) | null>(null);

View file

@ -1,45 +1,9 @@
'use client';
import { useMemo } from 'react';
import { useAuthConfig } from '@/contexts/AutoRateLimitContext';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { getAuthClient } from '@/lib/auth-client';
/**
* Hook that provides auth client methods configured with the correct baseUrl from server.
* Use this hook in components that need to call auth methods (signIn, signUp, etc.)
*/
export function useAuth() {
const { baseUrl, authEnabled } = useAuthConfig();
const client = useMemo(() => {
if (!authEnabled || !baseUrl) return null;
return getAuthClient(baseUrl);
}, [baseUrl, authEnabled]);
if (!client) {
// Safe no-op implementation when auth is disabled
return {
authEnabled: false,
baseUrl: null,
signIn: async () => ({ data: null, error: null }),
signUp: async () => ({ data: null, error: null }),
signOut: async () => ({ data: null, error: null }),
useSession: () => ({ data: null, isPending: false, error: null }),
getSession: async () => ({ data: null, error: null }),
};
}
return {
authEnabled,
baseUrl,
signIn: client.signIn,
signUp: client.signUp,
signOut: client.signOut,
useSession: client.useSession,
getSession: client.getSession,
};
}
/**
* Hook for session that uses the correct baseUrl from context
*/

View file

@ -14,14 +14,14 @@ const clientCache = new Map<string, ReturnType<typeof createAuthClientWithUrl>>(
/**
* Factory function to get auth client with specific baseUrl.
* In most cases, you should use the useAuth() hook instead of calling this directly.
* In components, prefer reading `baseUrl` from `useAuthConfig()` and then calling `getAuthClient(baseUrl)`.
* @param baseUrl - The auth server base URL. If null, will throw an error.
*/
export function getAuthClient(baseUrl: string | null) {
if (!baseUrl) {
throw new Error(
'Cannot create auth client without baseUrl. ' +
'Use the useAuth() hook in components to get the properly configured client.'
'Use useAuthConfig() in components to get the properly configured baseUrl.'
);
}
@ -30,4 +30,4 @@ export function getAuthClient(baseUrl: string | null) {
}
return clientCache.get(baseUrl)!;
}
}

View file

@ -1,11 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('API health checks', () => {
test('GET /api/tts/voices returns 200 and a non-empty voices array', async ({ request }) => {
const res = await request.get('/api/tts/voices');
expect(res.ok()).toBeTruthy();
const json = await res.json();
expect(Array.isArray(json.voices)).toBeTruthy();
expect(json.voices.length).toBeGreaterThan(0);
});
});

View file

@ -6,6 +6,50 @@ const DIR = './tests/files/';
const TTS_MOCK_PATH = path.join(__dirname, 'files', 'sample.mp3');
let ttsMockBuffer: Buffer | null = null;
type RateLimitStatusResponse = {
authEnabled: boolean;
userType?: 'anonymous' | 'authenticated' | 'unauthenticated';
};
async function getRateLimitStatus(page: Page): Promise<RateLimitStatusResponse | null> {
try {
const res = await page.request.get('/api/rate-limit/status');
if (!res.ok()) return null;
return (await res.json()) as RateLimitStatusResponse;
} catch {
return null;
}
}
async function ensureAnonymousSession(page: Page): Promise<void> {
const initial = await getRateLimitStatus(page);
if (!initial) return;
if (!initial.authEnabled) return;
if (initial.userType && initial.userType !== 'unauthenticated') return;
// Create a session cookie for this test context.
// This avoids races where the app makes authenticated API calls before AuthLoader finishes.
try {
await page.request.post('/api/auth/sign-in/anonymous', { data: {} });
} catch {
// ignore
}
// Wait until the server sees us as anonymous/authenticated (i.e. cookie persisted).
const deadline = Date.now() + 15_000;
// eslint-disable-next-line no-constant-condition
while (true) {
const next = await getRateLimitStatus(page);
if (next && (!next.authEnabled || (next.userType && next.userType !== 'unauthenticated'))) {
return;
}
if (Date.now() > deadline) {
throw new Error('Timed out waiting for anonymous auth session in tests');
}
await page.waitForTimeout(200);
}
}
async function ensureTtsRouteMock(page: Page) {
if (!ttsMockBuffer) {
ttsMockBuffer = fs.readFileSync(TTS_MOCK_PATH);
@ -119,10 +163,21 @@ export async function setupTest(page: Page) {
// Mock the TTS API so tests don't hit the real TTS service.
await ensureTtsRouteMock(page);
// If auth is enabled, establish an anonymous session BEFORE navigation.
// This keeps each test self-contained (no shared storageState) while ensuring
// server routes that require auth don't intermittently 401 during app startup.
await ensureAnonymousSession(page);
// Navigate to the home page before each test
await page.goto('/');
await page.waitForLoadState('networkidle');
// AuthLoader may show a full-screen overlay while session is loading.
// Wait for it to be gone before interacting with underlying UI.
await page
.waitForSelector('.fixed.inset-0.bg-base.z-50', { state: 'detached', timeout: 15_000 })
.catch(() => { });
// Privacy modal should come first in onboarding.
// Be tolerant if it's already accepted (e.g., reused context).
const privacyBtn = page.getByRole('button', { name: 'I Understand' });
@ -219,7 +274,7 @@ export async function deleteDocumentByName(page: Page, fileName: string) {
// Open Settings modal and navigate to Documents tab
export async function openSettingsDocumentsTab(page: Page) {
await page.getByRole('button', { name: 'Settings' }).click();
await page.getByRole('tab', { name: '📄 Documents' }).click();
await page.getByRole('tab', { name: '📄 Docs' }).click();
}
// Delete all local documents through Settings and close dialogs

View file

@ -61,9 +61,6 @@ test.describe('Document Upload Tests', () => {
});
test('uploads and converts a DOCX document', async ({ page }) => {
// This test only runs in development mode
test.skip(process.env.NODE_ENV === 'production', 'DOCX upload is only available in development mode');
await uploadFile(page, 'sample.docx');
// Should see the converting message
await expect(page.getByText('Converting DOCX to PDF...')).toBeVisible();