Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/admin/src/components/AdminDetailDrawer.web.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,18 +308,19 @@ describe('AdminDetailDrawer', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it('does not trap Tab when focus is not on the last element', () => {
it('does not trap Tab when focus is not on the last element', async () => {
const user = userEvent.setup();
render(
<AdminDetailDrawer open title='User' onClose={vi.fn()}>
<input aria-label='Notes' />
<button type='button'>Save</button>
</AdminDetailDrawer>
);
const notes = screen.getByLabelText('Notes');
notes.focus();
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
);
const save = screen.getByRole('button', { name: 'Save' });
await user.click(notes);
expect(document.activeElement).toBe(notes);
await user.tab();
expect(document.activeElement).toBe(save);
});
});
212 changes: 130 additions & 82 deletions packages/billing/src/BillingProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import {
testSubscription,
} from './test/billingFixtures.js';

type ConsoleErrorSpy = ReturnType<typeof vi.spyOn<typeof console, 'error'>>;

function expectNoUnmountedConsoleWarnings(consoleSpy: ConsoleErrorSpy) {
const unmountedWarnings = consoleSpy.mock.calls.filter(args =>
args.some(arg => String(arg).toLowerCase().includes('unmounted'))
);
expect(unmountedWarnings).toHaveLength(0);
}

function Reader() {
const { userId, subscription } = useBillingContext();
return (
Expand Down Expand Up @@ -591,99 +600,138 @@ describe('BillingProvider', () => {
data: ReturnType<typeof testPlan> | null;
error: null;
}) => void = () => {};
auth.state.session = { user: { id: 'u-plan-unmount' } };
const row = {
...testSubscription(),
user_id: 'u-plan-unmount',
plan_id: 'plan_free',
};
db.maybeSingle.mockResolvedValue({ data: row, error: null });
db.planMaybeSingle.mockImplementationOnce(
() =>
new Promise(resolve => {
resolvePlan = resolve;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<PlanReader />
</BillingProvider>
);
await waitFor(() => expect(db.planMaybeSingle).toHaveBeenCalled());
unmount();
resolvePlan({ data: testPlan({ display_name: 'Late plan' }), error: null });
await Promise.resolve();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
auth.state.session = { user: { id: 'u-plan-unmount' } };
const row = {
...testSubscription(),
user_id: 'u-plan-unmount',
plan_id: 'plan_free',
};
db.maybeSingle.mockResolvedValue({ data: row, error: null });
db.planMaybeSingle.mockImplementationOnce(
() =>
new Promise(resolve => {
resolvePlan = resolve;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<PlanReader />
</BillingProvider>
);
await waitFor(() => expect(db.planMaybeSingle).toHaveBeenCalled());
unmount();
resolvePlan({
data: testPlan({ display_name: 'Late plan' }),
error: null,
});
await act(async () => {
await Promise.resolve();
});

expectNoUnmountedConsoleWarnings(consoleSpy);
} finally {
consoleSpy.mockRestore();
}
});

it('ignores ensure_billing_subscription result after unmount', async () => {
let resolveRpc: (value: { error: null }) => void = () => {};
auth.state.session = { user: { id: 'u-rpc-unmount' } };
db.rpc.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveRpc = resolve;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<Reader />
</BillingProvider>
);
await waitFor(() =>
expect(screen.getByTestId('uid').textContent).toBe('u-rpc-unmount')
);
await waitFor(() => expect(db.rpc).toHaveBeenCalled());
unmount();
resolveRpc({ error: null });
await Promise.resolve();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
auth.state.session = { user: { id: 'u-rpc-unmount' } };
db.rpc.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveRpc = resolve;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<Reader />
</BillingProvider>
);
await waitFor(() =>
expect(screen.getByTestId('uid').textContent).toBe('u-rpc-unmount')
);
await waitFor(() => expect(db.rpc).toHaveBeenCalled());
unmount();
resolveRpc({ error: null });
await act(async () => {
await Promise.resolve();
});

expectNoUnmountedConsoleWarnings(consoleSpy);
} finally {
consoleSpy.mockRestore();
}
});

it('ignores plan query errors after unmount', async () => {
let rejectPlan: (error: Error) => void = () => {};
auth.state.session = { user: { id: 'u-plan-err-unmount' } };
const row = {
...testSubscription(),
user_id: 'u-plan-err-unmount',
plan_id: 'plan_free',
};
db.maybeSingle.mockResolvedValue({ data: row, error: null });
db.planMaybeSingle.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectPlan = reject;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<PlanReader />
</BillingProvider>
);
await waitFor(() => expect(db.planMaybeSingle).toHaveBeenCalled());
unmount();
rejectPlan(new Error('late plan fail'));
await Promise.resolve();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
auth.state.session = { user: { id: 'u-plan-err-unmount' } };
const row = {
...testSubscription(),
user_id: 'u-plan-err-unmount',
plan_id: 'plan_free',
};
db.maybeSingle.mockResolvedValue({ data: row, error: null });
db.planMaybeSingle.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectPlan = reject;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<PlanReader />
</BillingProvider>
);
await waitFor(() => expect(db.planMaybeSingle).toHaveBeenCalled());
unmount();
rejectPlan(new Error('late plan fail'));
await act(async () => {
await Promise.resolve();
});

expectNoUnmountedConsoleWarnings(consoleSpy);
} finally {
consoleSpy.mockRestore();
}
});

it('ignores ensure_billing_subscription errors after unmount', async () => {
let rejectRpc: (error: Error) => void = () => {};
auth.state.session = { user: { id: 'u-rpc-err-unmount' } };
db.rpc.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectRpc = reject;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<Reader />
</BillingProvider>
);
await waitFor(() =>
expect(screen.getByTestId('uid').textContent).toBe('u-rpc-err-unmount')
);
await waitFor(() => expect(db.rpc).toHaveBeenCalled());
unmount();
rejectRpc(new Error('late rpc fail'));
await Promise.resolve();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
auth.state.session = { user: { id: 'u-rpc-err-unmount' } };
db.rpc.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectRpc = reject;
})
);
const { unmount } = render(
<BillingProvider config={testBillingConfig} {...providerProps}>
<Reader />
</BillingProvider>
);
await waitFor(() =>
expect(screen.getByTestId('uid').textContent).toBe('u-rpc-err-unmount')
);
await waitFor(() => expect(db.rpc).toHaveBeenCalled());
unmount();
rejectRpc(new Error('late rpc fail'));
await act(async () => {
await Promise.resolve();
});

expectNoUnmountedConsoleWarnings(consoleSpy);
} finally {
consoleSpy.mockRestore();
}
});
});
34 changes: 25 additions & 9 deletions packages/waitlist/src/hooks/useSignupMode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { renderHook, waitFor } from '@testing-library/react';
import { useSignupMode } from './useSignupMode.js';
import * as client from '../waitlistClient.js';

type ConsoleErrorSpy = ReturnType<typeof vi.spyOn<typeof console, 'error'>>;

function expectNoUnmountedConsoleWarnings(consoleSpy: ConsoleErrorSpy) {
const unmountedWarnings = consoleSpy.mock.calls.filter(args =>
args.some(arg => String(arg).toLowerCase().includes('unmounted'))
);
expect(unmountedWarnings).toHaveLength(0);
}

describe('useSignupMode', () => {
it('loads public settings and exposes mode flags', async () => {
vi.spyOn(client, 'getPublicWaitlistSettings').mockResolvedValue({
Expand Down Expand Up @@ -31,14 +40,21 @@ describe('useSignupMode', () => {
resolveSettings = resolve;
})
);
const supabase = {} as never;
const { unmount } = renderHook(() => useSignupMode(supabase));
await waitFor(() =>
expect(client.getPublicWaitlistSettings).toHaveBeenCalled()
);
unmount();
resolveSettings({ signup_mode: 'open', copy: {}, metadata_schema: [] });
await Promise.resolve();
vi.restoreAllMocks();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
try {
const supabase = {} as never;
const { unmount } = renderHook(() => useSignupMode(supabase));
await waitFor(() =>
expect(client.getPublicWaitlistSettings).toHaveBeenCalled()
);
unmount();
resolveSettings({ signup_mode: 'open', copy: {}, metadata_schema: [] });
await Promise.resolve();

expectNoUnmountedConsoleWarnings(consoleSpy);
} finally {
consoleSpy.mockRestore();
vi.restoreAllMocks();
}
});
});
Loading