Refactor OAuth provider tests for consistency and readability

- Updated test files for Discord, GitHub, Google, and Twitter to improve code consistency by standardizing async function formatting and import statements.
- Enhanced readability by restructuring promise resolutions and waitFor assertions.
- Ensured uniformity in the handling of button click events across all OAuth provider tests.
This commit is contained in:
Steven Enamakel
2026-03-30 07:47:53 -07:00
parent 513562e814
commit 3b9fd2e446
5 changed files with 108 additions and 92 deletions
+27 -20
View File
@@ -9,13 +9,13 @@
* - Error handling when backend URL lookup fails
* - dev-mode URL construction (?responseType=json)
*/
import type { ComponentProps } from 'react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../src/test/test-utils';
import OAuthProviderButton from '../src/components/oauth/OAuthProviderButton';
import { oauthProviderConfigs } from '../src/components/oauth/providerConfigs';
import { renderWithProviders } from '../src/test/test-utils';
// ---------------------------------------------------------------------------
// Module mocks
@@ -29,7 +29,7 @@ const { mockGetBackendUrl, mockOpenUrl, mockIsTauri } = vi.hoisted(() => ({
vi.mock('../src/services/backendUrl', () => ({ getBackendUrl: mockGetBackendUrl }));
vi.mock('../src/utils/openUrl', () => ({ openUrl: mockOpenUrl }));
vi.mock('../src/utils/tauriCommands', async (importOriginal) => {
vi.mock('../src/utils/tauriCommands', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, isTauri: mockIsTauri };
});
@@ -43,7 +43,10 @@ const discordConfig = oauthProviderConfigs.find(p => p.id === 'discord')!;
const renderDiscordButton = (props: Partial<ComponentProps<typeof OAuthProviderButton>> = {}) =>
renderWithProviders(<OAuthProviderButton provider={discordConfig} {...props} />);
const clickButton = (btn: HTMLElement) => act(async () => { fireEvent.click(btn); });
const clickButton = (btn: HTMLElement) =>
act(async () => {
fireEvent.click(btn);
});
// ---------------------------------------------------------------------------
// Rendering
@@ -115,9 +118,7 @@ describe('OAuthProviderButton (Discord) — web OAuth flow', () => {
renderDiscordButton();
await clickButton(screen.getByRole('button', { name: /discord/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockOpenUrl).not.toHaveBeenCalled();
});
@@ -125,9 +126,7 @@ describe('OAuthProviderButton (Discord) — web OAuth flow', () => {
renderDiscordButton();
await clickButton(screen.getByRole('button', { name: /discord/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
});
});
@@ -180,7 +179,11 @@ describe('OAuthProviderButton (Discord) — Tauri OAuth flow', () => {
describe('OAuthProviderButton (Discord) — loading state', () => {
it('shows spinner and "Connecting..." while getBackendUrl is pending', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderDiscordButton();
@@ -191,12 +194,18 @@ describe('OAuthProviderButton (Discord) — loading state', () => {
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
expect(button).toBeDisabled();
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
it('ignores a second click while already loading', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderDiscordButton();
@@ -208,7 +217,9 @@ describe('OAuthProviderButton (Discord) — loading state', () => {
fireEvent.click(button);
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
});
@@ -242,9 +253,7 @@ describe('OAuthProviderButton (Discord) — error handling', () => {
renderDiscordButton();
await clickButton(screen.getByRole('button', { name: /discord/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /discord/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /discord/i })).toBeEnabled());
expect((window.location as unknown as { href: string }).href).toBe('');
(window as unknown as Record<string, unknown>).location = originalLocation;
@@ -257,9 +266,7 @@ describe('OAuthProviderButton (Discord) — error handling', () => {
renderDiscordButton();
await clickButton(screen.getByRole('button', { name: /discord/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /discord/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /discord/i })).toBeEnabled());
expect(mockOpenUrl).not.toHaveBeenCalled();
});
+27 -20
View File
@@ -9,13 +9,13 @@
* - Error handling when backend URL lookup fails
* - dev-mode URL construction (?responseType=json)
*/
import type { ComponentProps } from 'react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../src/test/test-utils';
import OAuthProviderButton from '../src/components/oauth/OAuthProviderButton';
import { oauthProviderConfigs } from '../src/components/oauth/providerConfigs';
import { renderWithProviders } from '../src/test/test-utils';
// ---------------------------------------------------------------------------
// Module mocks
@@ -29,7 +29,7 @@ const { mockGetBackendUrl, mockOpenUrl, mockIsTauri } = vi.hoisted(() => ({
vi.mock('../src/services/backendUrl', () => ({ getBackendUrl: mockGetBackendUrl }));
vi.mock('../src/utils/openUrl', () => ({ openUrl: mockOpenUrl }));
vi.mock('../src/utils/tauriCommands', async (importOriginal) => {
vi.mock('../src/utils/tauriCommands', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, isTauri: mockIsTauri };
});
@@ -43,7 +43,10 @@ const githubConfig = oauthProviderConfigs.find(p => p.id === 'github')!;
const renderGitHubButton = (props: Partial<ComponentProps<typeof OAuthProviderButton>> = {}) =>
renderWithProviders(<OAuthProviderButton provider={githubConfig} {...props} />);
const clickButton = (btn: HTMLElement) => act(async () => { fireEvent.click(btn); });
const clickButton = (btn: HTMLElement) =>
act(async () => {
fireEvent.click(btn);
});
// ---------------------------------------------------------------------------
// Rendering
@@ -115,9 +118,7 @@ describe('OAuthProviderButton (GitHub) — web OAuth flow', () => {
renderGitHubButton();
await clickButton(screen.getByRole('button', { name: /github/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockOpenUrl).not.toHaveBeenCalled();
});
@@ -125,9 +126,7 @@ describe('OAuthProviderButton (GitHub) — web OAuth flow', () => {
renderGitHubButton();
await clickButton(screen.getByRole('button', { name: /github/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
});
});
@@ -180,7 +179,11 @@ describe('OAuthProviderButton (GitHub) — Tauri OAuth flow', () => {
describe('OAuthProviderButton (GitHub) — loading state', () => {
it('shows spinner and "Connecting..." while getBackendUrl is pending', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderGitHubButton();
@@ -191,12 +194,18 @@ describe('OAuthProviderButton (GitHub) — loading state', () => {
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
expect(button).toBeDisabled();
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
it('ignores a second click while already loading', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderGitHubButton();
@@ -208,7 +217,9 @@ describe('OAuthProviderButton (GitHub) — loading state', () => {
fireEvent.click(button);
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
});
@@ -242,9 +253,7 @@ describe('OAuthProviderButton (GitHub) — error handling', () => {
renderGitHubButton();
await clickButton(screen.getByRole('button', { name: /github/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /github/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /github/i })).toBeEnabled());
expect((window.location as unknown as { href: string }).href).toBe('');
(window as unknown as Record<string, unknown>).location = originalLocation;
@@ -257,9 +266,7 @@ describe('OAuthProviderButton (GitHub) — error handling', () => {
renderGitHubButton();
await clickButton(screen.getByRole('button', { name: /github/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /github/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /github/i })).toBeEnabled());
expect(mockOpenUrl).not.toHaveBeenCalled();
});
+26 -29
View File
@@ -9,14 +9,14 @@
* - Error handling when the backend URL lookup fails
* - dev-mode URL construction (responseType=json query param)
*/
import type { ComponentProps } from 'react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../src/test/test-utils';
import OAuthLoginSection from '../src/components/oauth/OAuthLoginSection';
import OAuthProviderButton from '../src/components/oauth/OAuthProviderButton';
import { oauthProviderConfigs } from '../src/components/oauth/providerConfigs';
import { renderWithProviders } from '../src/test/test-utils';
// ---------------------------------------------------------------------------
// Module mocks
@@ -30,15 +30,11 @@ const { mockGetBackendUrl, mockOpenUrl, mockIsTauri } = vi.hoisted(() => ({
mockIsTauri: vi.fn(),
}));
vi.mock('../src/services/backendUrl', () => ({
getBackendUrl: mockGetBackendUrl,
}));
vi.mock('../src/services/backendUrl', () => ({ getBackendUrl: mockGetBackendUrl }));
vi.mock('../src/utils/openUrl', () => ({
openUrl: mockOpenUrl,
}));
vi.mock('../src/utils/openUrl', () => ({ openUrl: mockOpenUrl }));
vi.mock('../src/utils/tauriCommands', async (importOriginal) => {
vi.mock('../src/utils/tauriCommands', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, isTauri: mockIsTauri };
});
@@ -58,7 +54,10 @@ const renderGoogleButton = (props: Partial<ComponentProps<typeof OAuthProviderBu
renderWithProviders(<OAuthProviderButton provider={googleConfig} {...props} />);
// act() with an async callback returns Promise<void>, making await valid.
const clickButton = (btn: HTMLElement) => act(async () => { fireEvent.click(btn); });
const clickButton = (btn: HTMLElement) =>
act(async () => {
fireEvent.click(btn);
});
// ---------------------------------------------------------------------------
// OAuthLoginSection — rendering
@@ -170,9 +169,7 @@ describe('OAuthProviderButton (Google) — web OAuth flow', () => {
renderGoogleButton();
await clickButton(screen.getByRole('button', { name: /google/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockOpenUrl).not.toHaveBeenCalled();
});
@@ -180,9 +177,7 @@ describe('OAuthProviderButton (Google) — web OAuth flow', () => {
renderGoogleButton();
await clickButton(screen.getByRole('button', { name: /google/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
});
});
@@ -227,7 +222,9 @@ describe('OAuthProviderButton (Google) — loading state', () => {
it('shows spinner and "Connecting..." text while login is in progress', async () => {
let resolveBackendUrl!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => { resolveBackendUrl = res; })
new Promise<string>(res => {
resolveBackendUrl = res;
})
);
mockIsTauri.mockReturnValue(false);
@@ -241,13 +238,17 @@ describe('OAuthProviderButton (Google) — loading state', () => {
expect(button).toBeDisabled();
// Settle the promise so React doesn't warn about state updates after unmount
await act(async () => { resolveBackendUrl('http://localhost:5005'); });
await act(async () => {
resolveBackendUrl('http://localhost:5005');
});
});
it('does not respond to a second click while already loading', async () => {
let resolveBackendUrl!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => { resolveBackendUrl = res; })
new Promise<string>(res => {
resolveBackendUrl = res;
})
);
mockIsTauri.mockReturnValue(false);
@@ -261,7 +262,9 @@ describe('OAuthProviderButton (Google) — loading state', () => {
fireEvent.click(button);
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
await act(async () => { resolveBackendUrl('http://localhost:5005'); });
await act(async () => {
resolveBackendUrl('http://localhost:5005');
});
});
it('remains in loading state after successful Tauri openUrl (awaits deep-link callback)', async () => {
@@ -311,9 +314,7 @@ describe('OAuthProviderButton (Google) — error handling', () => {
renderGoogleButton();
await clickButton(screen.getByRole('button', { name: /google/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /google/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /google/i })).toBeEnabled());
expect((window.location as unknown as { href: string }).href).toBe('');
(window as unknown as Record<string, unknown>).location = originalLocation;
@@ -326,9 +327,7 @@ describe('OAuthProviderButton (Google) — error handling', () => {
renderGoogleButton();
await clickButton(screen.getByRole('button', { name: /google/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /google/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /google/i })).toBeEnabled());
expect(mockOpenUrl).not.toHaveBeenCalled();
});
@@ -371,9 +370,7 @@ describe('OAuthProviderButton (Google) — dev mode URL params', () => {
renderGoogleButton();
await clickButton(screen.getByRole('button', { name: /google/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect((window.location as unknown as { href: string }).href).toBe(
'https://api.example.com/auth/google/login?responseType=json'
);
+27 -20
View File
@@ -9,13 +9,13 @@
* - Error handling when backend URL lookup fails
* - dev-mode URL construction (?responseType=json)
*/
import type { ComponentProps } from 'react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../src/test/test-utils';
import OAuthProviderButton from '../src/components/oauth/OAuthProviderButton';
import { oauthProviderConfigs } from '../src/components/oauth/providerConfigs';
import { renderWithProviders } from '../src/test/test-utils';
// ---------------------------------------------------------------------------
// Module mocks
@@ -29,7 +29,7 @@ const { mockGetBackendUrl, mockOpenUrl, mockIsTauri } = vi.hoisted(() => ({
vi.mock('../src/services/backendUrl', () => ({ getBackendUrl: mockGetBackendUrl }));
vi.mock('../src/utils/openUrl', () => ({ openUrl: mockOpenUrl }));
vi.mock('../src/utils/tauriCommands', async (importOriginal) => {
vi.mock('../src/utils/tauriCommands', async importOriginal => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, isTauri: mockIsTauri };
});
@@ -43,7 +43,10 @@ const twitterConfig = oauthProviderConfigs.find(p => p.id === 'twitter')!;
const renderTwitterButton = (props: Partial<ComponentProps<typeof OAuthProviderButton>> = {}) =>
renderWithProviders(<OAuthProviderButton provider={twitterConfig} {...props} />);
const clickButton = (btn: HTMLElement) => act(async () => { fireEvent.click(btn); });
const clickButton = (btn: HTMLElement) =>
act(async () => {
fireEvent.click(btn);
});
// ---------------------------------------------------------------------------
// Rendering
@@ -115,9 +118,7 @@ describe('OAuthProviderButton (Twitter) — web OAuth flow', () => {
renderTwitterButton();
await clickButton(screen.getByRole('button', { name: /twitter/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockOpenUrl).not.toHaveBeenCalled();
});
@@ -125,9 +126,7 @@ describe('OAuthProviderButton (Twitter) — web OAuth flow', () => {
renderTwitterButton();
await clickButton(screen.getByRole('button', { name: /twitter/i }));
await waitFor(() =>
expect((window.location as unknown as { href: string }).href).not.toBe('')
);
await waitFor(() => expect((window.location as unknown as { href: string }).href).not.toBe(''));
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
});
});
@@ -180,7 +179,11 @@ describe('OAuthProviderButton (Twitter) — Tauri OAuth flow', () => {
describe('OAuthProviderButton (Twitter) — loading state', () => {
it('shows spinner and "Connecting..." while getBackendUrl is pending', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderTwitterButton();
@@ -191,12 +194,18 @@ describe('OAuthProviderButton (Twitter) — loading state', () => {
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
expect(button).toBeDisabled();
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
it('ignores a second click while already loading', async () => {
let resolve!: (_v: string) => void;
mockGetBackendUrl.mockReturnValue(new Promise<string>(res => { resolve = res; }));
mockGetBackendUrl.mockReturnValue(
new Promise<string>(res => {
resolve = res;
})
);
mockIsTauri.mockReturnValue(false);
renderTwitterButton();
@@ -208,7 +217,9 @@ describe('OAuthProviderButton (Twitter) — loading state', () => {
fireEvent.click(button);
expect(mockGetBackendUrl).toHaveBeenCalledTimes(1);
await act(async () => { resolve('http://localhost:5005'); });
await act(async () => {
resolve('http://localhost:5005');
});
});
});
@@ -242,9 +253,7 @@ describe('OAuthProviderButton (Twitter) — error handling', () => {
renderTwitterButton();
await clickButton(screen.getByRole('button', { name: /twitter/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /twitter/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /twitter/i })).toBeEnabled());
expect((window.location as unknown as { href: string }).href).toBe('');
(window as unknown as Record<string, unknown>).location = originalLocation;
@@ -257,9 +266,7 @@ describe('OAuthProviderButton (Twitter) — error handling', () => {
renderTwitterButton();
await clickButton(screen.getByRole('button', { name: /twitter/i }));
await waitFor(() =>
expect(screen.getByRole('button', { name: /twitter/i })).toBeEnabled()
);
await waitFor(() => expect(screen.getByRole('button', { name: /twitter/i })).toBeEnabled());
expect(mockOpenUrl).not.toHaveBeenCalled();
});
+1 -3
View File
@@ -1,7 +1,5 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom", "node"]
},
"compilerOptions": { "types": ["vitest/globals", "@testing-library/jest-dom", "node"] },
"include": ["../src", "./*.test.ts", "./*.test.tsx"]
}