fix: add timeout to Turnstile polling loop to prevent infinite retries
When the Cloudflare Turnstile script fails to load (e.g., 503 from CDN), the polling interval ran forever, causing the page to appear stuck in a refresh loop. Now stops after 100 attempts (10s) and calls onError. Also adds dedicated unit tests for the Turnstile component covering immediate render, delayed load, timeout, and cleanup-during-polling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>master
parent
bc68434ed8
commit
d2851c019e
@ -0,0 +1,99 @@
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { render, cleanup } from '@solidjs/testing-library';
|
||||
import Turnstile from '~/components/Turnstile';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
delete (window as any).turnstile;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
delete (window as any).turnstile;
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('Turnstile component', () => {
|
||||
it('should render widget immediately when window.turnstile is available', () => {
|
||||
const onToken = vi.fn();
|
||||
const renderMock = vi.fn((_el: HTMLElement, opts: any) => {
|
||||
opts.callback('test-token');
|
||||
return 'widget-1';
|
||||
});
|
||||
|
||||
(window as any).turnstile = {
|
||||
render: renderMock,
|
||||
reset: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
render(() => <Turnstile onToken={onToken} />);
|
||||
|
||||
expect(renderMock).toHaveBeenCalledTimes(1);
|
||||
expect(onToken).toHaveBeenCalledWith('test-token');
|
||||
});
|
||||
|
||||
it('should poll and render when window.turnstile appears after delay', () => {
|
||||
const onToken = vi.fn();
|
||||
const renderMock = vi.fn((_el: HTMLElement, opts: any) => {
|
||||
opts.callback('delayed-token');
|
||||
return 'widget-2';
|
||||
});
|
||||
|
||||
render(() => <Turnstile onToken={onToken} />);
|
||||
|
||||
// Turnstile not yet available — advance a few ticks
|
||||
vi.advanceTimersByTime(300);
|
||||
expect(onToken).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate script loading
|
||||
(window as any).turnstile = {
|
||||
render: renderMock,
|
||||
reset: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
expect(renderMock).toHaveBeenCalledTimes(1);
|
||||
expect(onToken).toHaveBeenCalledWith('delayed-token');
|
||||
});
|
||||
|
||||
it('should call onError after 10 seconds if window.turnstile never loads', () => {
|
||||
const onToken = vi.fn();
|
||||
const onError = vi.fn();
|
||||
|
||||
render(() => <Turnstile onToken={onToken} onError={onError} />);
|
||||
|
||||
// Advance to just before timeout (99 ticks = 9.9s)
|
||||
vi.advanceTimersByTime(9900);
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
|
||||
// Advance past timeout (100th tick = 10s)
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onToken).not.toHaveBeenCalled();
|
||||
|
||||
// Verify interval is cleared — no further calls after timeout
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should clear interval on unmount during polling', () => {
|
||||
const onToken = vi.fn();
|
||||
const onError = vi.fn();
|
||||
|
||||
const { unmount } = render(() => (
|
||||
<Turnstile onToken={onToken} onError={onError} />
|
||||
));
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
unmount();
|
||||
|
||||
// Advance past what would be the timeout — nothing should fire
|
||||
vi.advanceTimersByTime(15000);
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
expect(onToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue