fix(tests): try to get tests working again
This commit is contained in:
parent
6089d2c445
commit
20111b1e36
18 changed files with 97 additions and 92 deletions
|
|
@ -17,7 +17,7 @@ import type { TTSAudiobookChapter } from '@/types/tts';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/dexie';
|
||||||
import { RateLimitBanner } from '@/components/rate-limit-banner';
|
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;
|
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 router = useRouter();
|
||||||
const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter } = useEPUB();
|
const { setCurrentDocument, currDocName, clearCurrDoc, createFullAudioBook: createEPUBAudioBook, regenerateChapter: regenerateEPUBChapter } = useEPUB();
|
||||||
const { stop } = useTTS();
|
const { stop } = useTTS();
|
||||||
const { isAtLimit } = useAutoRateLimit();
|
const { isAtLimit } = useAuthRateLimit();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/dexie';
|
||||||
import { DocumentHeaderMenu } from '@/components/DocumentHeaderMenu';
|
import { DocumentHeaderMenu } from '@/components/DocumentHeaderMenu';
|
||||||
import { RateLimitBanner } from '@/components/rate-limit-banner';
|
import { RateLimitBanner } from '@/components/rate-limit-banner';
|
||||||
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
|
|
||||||
export default function HTMLPage() {
|
export default function HTMLPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
|
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
|
||||||
const { stop } = useTTS();
|
const { stop } = useTTS();
|
||||||
const { isAtLimit } = useAutoRateLimit();
|
const { isAtLimit } = useAuthRateLimit();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
|
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/dexie';
|
||||||
import { RateLimitBanner } from '@/components/rate-limit-banner';
|
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;
|
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 router = useRouter();
|
||||||
const { setCurrentDocument, currDocName, clearCurrDoc, currDocPage, currDocPages, createFullAudioBook: createPDFAudioBook, regenerateChapter: regeneratePDFChapter } = usePDF();
|
const { setCurrentDocument, currDocName, clearCurrDoc, currDocPage, currDocPages, createFullAudioBook: createPDFAudioBook, regenerateChapter: regeneratePDFChapter } = usePDF();
|
||||||
const { stop } = useTTS();
|
const { stop } = useTTS();
|
||||||
const { isAtLimit } = useAutoRateLimit();
|
const { isAtLimit } = useAuthRateLimit();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [zoomLevel, setZoomLevel] = useState<number>(100);
|
const [zoomLevel, setZoomLevel] = useState<number>(100);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { TTSProvider } from '@/contexts/TTSContext';
|
||||||
import { ThemeProvider } from '@/contexts/ThemeContext';
|
import { ThemeProvider } from '@/contexts/ThemeContext';
|
||||||
import { ConfigProvider } from '@/contexts/ConfigContext';
|
import { ConfigProvider } from '@/contexts/ConfigContext';
|
||||||
import { HTMLProvider } from '@/contexts/HTMLContext';
|
import { HTMLProvider } from '@/contexts/HTMLContext';
|
||||||
import { AutoRateLimitProvider } from '@/contexts/AutoRateLimitContext';
|
import { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext';
|
||||||
import { PrivacyPopup } from '@/components/privacy-popup';
|
import { PrivacyPopup } from '@/components/privacy-popup';
|
||||||
|
|
||||||
interface ProvidersProps {
|
interface ProvidersProps {
|
||||||
|
|
@ -18,7 +18,7 @@ interface ProvidersProps {
|
||||||
|
|
||||||
export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps) {
|
export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps) {
|
||||||
return (
|
return (
|
||||||
<AutoRateLimitProvider authEnabled={authEnabled} authBaseUrl={authBaseUrl}>
|
<AuthRateLimitProvider authEnabled={authEnabled} authBaseUrl={authBaseUrl}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<DocumentProvider>
|
<DocumentProvider>
|
||||||
|
|
@ -35,6 +35,6 @@ export function Providers({ children, authEnabled, authBaseUrl }: ProvidersProps
|
||||||
</DocumentProvider>
|
</DocumentProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AutoRateLimitProvider>
|
</AuthRateLimitProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Button, Input } from '@headlessui/react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
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 { showPrivacyPopup } from '@/components/privacy-popup';
|
||||||
import { wasSignedOut, clearSignedOut } from '@/lib/session-utils';
|
import { wasSignedOut, clearSignedOut } from '@/lib/session-utils';
|
||||||
import { GithubIcon } from '@/components/icons/Icons';
|
import { GithubIcon } from '@/components/icons/Icons';
|
||||||
|
|
@ -32,7 +32,7 @@ function SignInContent() {
|
||||||
const [justSignedOut, setJustSignedOut] = useState(false);
|
const [justSignedOut, setJustSignedOut] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { authEnabled, baseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl } = useAuthConfig();
|
||||||
const { refresh: refreshRateLimit } = useAutoRateLimit();
|
const { refresh: refreshRateLimit } = useAuthRateLimit();
|
||||||
|
|
||||||
const isAnyLoading = loadingEmail || loadingGithub || loadingGuest;
|
const isAnyLoading = loadingEmail || loadingGithub || loadingGuest;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Button, Input } from '@headlessui/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
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 { showPrivacyPopup } from '@/components/privacy-popup';
|
||||||
import { LoadingSpinner } from '@/components/Spinner';
|
import { LoadingSpinner } from '@/components/Spinner';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
@ -19,7 +19,7 @@ export default function SignUpPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { authEnabled, baseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl } = useAuthConfig();
|
||||||
const { refresh: refreshRateLimit } = useAutoRateLimit();
|
const { refresh: refreshRateLimit } = useAuthRateLimit();
|
||||||
|
|
||||||
// Check if auth is enabled, redirect home if not
|
// Check if auth is enabled, redirect home if not
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { GithubIcon } from '@/components/icons/Icons'
|
import { GithubIcon } from '@/components/icons/Icons'
|
||||||
import { showPrivacyPopup } from '@/components/privacy-popup'
|
import { showPrivacyPopup } from '@/components/privacy-popup'
|
||||||
import { useAuthConfig } from '@/contexts/AutoRateLimitContext'
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext'
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const { authEnabled } = useAuthConfig();
|
const { authEnabled } = useAuthConfig();
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import { BaseDocument } from '@/types/documents';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/auth-client';
|
||||||
import { useAuthSession } from '@/hooks/useAuth';
|
import { useAuthSession } from '@/hooks/useAuth';
|
||||||
import { clearSignedOut } from '@/lib/session-utils';
|
import { clearSignedOut } from '@/lib/session-utils';
|
||||||
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { showPrivacyPopup } from '@/components/privacy-popup';
|
import { showPrivacyPopup } from '@/components/privacy-popup';
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false);
|
const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false);
|
||||||
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
|
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
|
||||||
const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
|
||||||
const { refresh: refreshRateLimit } = useAutoRateLimit();
|
const { refresh: refreshRateLimit } = useAuthRateLimit();
|
||||||
const { data: session } = useAuthSession();
|
const { data: session } = useAuthSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, ReactNode } from 'react';
|
import { useEffect, useState, ReactNode } from 'react';
|
||||||
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useAuthSession } from '@/hooks/useAuth';
|
import { useAuthSession } from '@/hooks/useAuth';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/auth-client';
|
||||||
import { LoadingSpinner } from '@/components/Spinner';
|
import { LoadingSpinner } from '@/components/Spinner';
|
||||||
|
|
||||||
export function AuthLoader({ children }: { children: ReactNode }) {
|
export function AuthLoader({ children }: { children: ReactNode }) {
|
||||||
const { authEnabled, baseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl } = useAuthConfig();
|
||||||
const { refresh: refreshRateLimit } = useAutoRateLimit();
|
const { refresh: refreshRateLimit } = useAuthRateLimit();
|
||||||
const { data: session, isPending } = useAuthSession();
|
const { data: session, isPending } = useAuthSession();
|
||||||
const [isAutoLoggingIn, setIsAutoLoggingIn] = useState(false);
|
const [isAutoLoggingIn, setIsAutoLoggingIn] = useState(false);
|
||||||
const [isCheckingSignOut, setIsCheckingSignOut] = useState(true);
|
const [isCheckingSignOut, setIsCheckingSignOut] = useState(true);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuthConfig, useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useAuthSession } from '@/hooks/useAuth';
|
import { useAuthSession } from '@/hooks/useAuth';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/auth-client';
|
||||||
import { clearSignedOut } from '@/lib/session-utils';
|
import { clearSignedOut } from '@/lib/session-utils';
|
||||||
|
|
@ -10,7 +10,7 @@ import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export function UserMenu({ className = '' }: { className?: string }) {
|
export function UserMenu({ className = '' }: { className?: string }) {
|
||||||
const { authEnabled, baseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl } = useAuthConfig();
|
||||||
const { refresh: refreshRateLimit } = useAutoRateLimit();
|
const { refresh: refreshRateLimit } = useAuthRateLimit();
|
||||||
const { data: session, isPending } = useAuthSession();
|
const { data: session, isPending } = useAuthSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAutoRateLimit, formatCharCount } from '@/contexts/AutoRateLimitContext';
|
import { useAuthRateLimit, formatCharCount } from '@/contexts/AuthRateLimitContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface RateLimitBannerProps {
|
interface RateLimitBannerProps {
|
||||||
|
|
@ -8,7 +8,7 @@ interface RateLimitBannerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RateLimitBanner({ className = '' }: 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
|
// Don't show banner if auth is not enabled or if not at limit
|
||||||
if (!authEnabled || !status?.authEnabled || !isAtLimit) {
|
if (!authEnabled || !status?.authEnabled || !isAtLimit) {
|
||||||
|
|
@ -49,7 +49,7 @@ export function RateLimitBanner({ className = '' }: RateLimitBannerProps) {
|
||||||
* Compact version for inline display
|
* Compact version for inline display
|
||||||
*/
|
*/
|
||||||
export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
|
export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
|
||||||
const { status, isAtLimit, authEnabled } = useAutoRateLimit();
|
const { status, isAtLimit, authEnabled } = useAuthRateLimit();
|
||||||
|
|
||||||
// Don't show if auth is not enabled
|
// Don't show if auth is not enabled
|
||||||
if (!authEnabled || !status?.authEnabled) {
|
if (!authEnabled || !status?.authEnabled) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export interface RateLimitStatus {
|
||||||
authEnabled: boolean;
|
authEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoRateLimitContextType {
|
interface AuthRateLimitContextType {
|
||||||
// Auth Config
|
// Auth Config
|
||||||
authEnabled: boolean;
|
authEnabled: boolean;
|
||||||
authBaseUrl: string | null;
|
authBaseUrl: string | null;
|
||||||
|
|
@ -30,24 +30,24 @@ interface AutoRateLimitContextType {
|
||||||
triggerRateLimit: () => void;
|
triggerRateLimit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutoRateLimitContext = createContext<AutoRateLimitContextType | null>(null);
|
const AuthRateLimitContext = createContext<AuthRateLimitContextType | null>(null);
|
||||||
|
|
||||||
export function useAutoRateLimit(): AutoRateLimitContextType {
|
export function useAuthRateLimit(): AuthRateLimitContextType {
|
||||||
const context = useContext(AutoRateLimitContext);
|
const context = useContext(AuthRateLimitContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useAutoRateLimit must be used within a AutoRateLimitProvider');
|
throw new Error('useAuthRateLimit must be used within an AuthRateLimitProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export specific hooks for backward compatibility or convenience if needed
|
// Re-export specific hooks for backward compatibility or convenience if needed
|
||||||
export function useAuthConfig() {
|
export function useAuthConfig() {
|
||||||
const { authEnabled, authBaseUrl } = useAutoRateLimit();
|
const { authEnabled, authBaseUrl } = useAuthRateLimit();
|
||||||
return { authEnabled, baseUrl: authBaseUrl };
|
return { authEnabled, baseUrl: authBaseUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRateLimit() {
|
export function useRateLimit() {
|
||||||
return useAutoRateLimit();
|
return useAuthRateLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateTimeUntilReset(resetTime: Date): string {
|
function calculateTimeUntilReset(resetTime: Date): string {
|
||||||
|
|
@ -82,13 +82,13 @@ export function formatCharCount(count: number): string {
|
||||||
return count.toString();
|
return count.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoRateLimitProviderProps {
|
interface AuthRateLimitProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
authEnabled: boolean;
|
authEnabled: boolean;
|
||||||
authBaseUrl: string | null;
|
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 [status, setStatus] = useState<RateLimitStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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,
|
authEnabled,
|
||||||
authBaseUrl,
|
authBaseUrl,
|
||||||
status,
|
status,
|
||||||
|
|
@ -225,8 +225,8 @@ export function AutoRateLimitProvider({ children, authEnabled, authBaseUrl }: Au
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoRateLimitContext.Provider value={contextValue}>
|
<AuthRateLimitContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</AutoRateLimitContext.Provider>
|
</AuthRateLimitContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
|
||||||
import { withRetry, generateTTS, alignAudio } from '@/lib/client';
|
import { withRetry, generateTTS, alignAudio } from '@/lib/client';
|
||||||
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/nlp';
|
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/nlp';
|
||||||
import { isKokoroModel } from '@/utils/voice';
|
import { isKokoroModel } from '@/utils/voice';
|
||||||
import { useAutoRateLimit } from '@/contexts/AutoRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import type {
|
import type {
|
||||||
TTSLocation,
|
TTSLocation,
|
||||||
TTSSmartMergeResult,
|
TTSSmartMergeResult,
|
||||||
|
|
@ -299,7 +299,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
const audioContext = useAudioContext();
|
const audioContext = useAudioContext();
|
||||||
const audioCache = useAudioCache(25);
|
const audioCache = useAudioCache(25);
|
||||||
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl, configTTSProvider, configTTSModel);
|
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
|
// Add ref for location change handler
|
||||||
const locationChangeHandlerRef = useRef<((location: TTSLocation) => void) | null>(null);
|
const locationChangeHandlerRef = useRef<((location: TTSLocation) => void) | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAuthConfig } from '@/contexts/AutoRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
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
|
* Hook for session that uses the correct baseUrl from context
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ const clientCache = new Map<string, ReturnType<typeof createAuthClientWithUrl>>(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to get auth client with specific baseUrl.
|
* 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.
|
* @param baseUrl - The auth server base URL. If null, will throw an error.
|
||||||
*/
|
*/
|
||||||
export function getAuthClient(baseUrl: string | null) {
|
export function getAuthClient(baseUrl: string | null) {
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Cannot create auth client without baseUrl. ' +
|
'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.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -6,6 +6,50 @@ const DIR = './tests/files/';
|
||||||
const TTS_MOCK_PATH = path.join(__dirname, 'files', 'sample.mp3');
|
const TTS_MOCK_PATH = path.join(__dirname, 'files', 'sample.mp3');
|
||||||
let ttsMockBuffer: Buffer | null = null;
|
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) {
|
async function ensureTtsRouteMock(page: Page) {
|
||||||
if (!ttsMockBuffer) {
|
if (!ttsMockBuffer) {
|
||||||
ttsMockBuffer = fs.readFileSync(TTS_MOCK_PATH);
|
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.
|
// Mock the TTS API so tests don't hit the real TTS service.
|
||||||
await ensureTtsRouteMock(page);
|
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
|
// Navigate to the home page before each test
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
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.
|
// Privacy modal should come first in onboarding.
|
||||||
// Be tolerant if it's already accepted (e.g., reused context).
|
// Be tolerant if it's already accepted (e.g., reused context).
|
||||||
const privacyBtn = page.getByRole('button', { name: 'I Understand' });
|
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
|
// Open Settings modal and navigate to Documents tab
|
||||||
export async function openSettingsDocumentsTab(page: Page) {
|
export async function openSettingsDocumentsTab(page: Page) {
|
||||||
await page.getByRole('button', { name: 'Settings' }).click();
|
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
|
// Delete all local documents through Settings and close dialogs
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,6 @@ test.describe('Document Upload Tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uploads and converts a DOCX document', async ({ page }) => {
|
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');
|
await uploadFile(page, 'sample.docx');
|
||||||
// Should see the converting message
|
// Should see the converting message
|
||||||
await expect(page.getByText('Converting DOCX to PDF...')).toBeVisible();
|
await expect(page.getByText('Converting DOCX to PDF...')).toBeVisible();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue