mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
47d9cce936
- Add new fields to AppConfig type and buildAppConfig factory - Update FE-PAGE-ADMIN-018: heading changed to "Authentication Methods" - Update FE-PAGE-ADMIN-053: oidc_only toggle removed from OIDC panel - Update FE-PAGE-LOGIN-007/017: mocks now include password_login/oidc_login - Update ADMIN-SVC-049: updateOidcSettings no longer writes oidc_only
596 lines
21 KiB
TypeScript
596 lines
21 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { render, screen, waitFor, fireEvent } from '../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { server } from '../../tests/helpers/msw/server';
|
|
import { resetAllStores } from '../../tests/helpers/store';
|
|
import LoginPage from './LoginPage';
|
|
|
|
// LoginPage uses inline styles for labels (no htmlFor/id pairing).
|
|
// We find inputs by placeholder text.
|
|
const EMAIL_PLACEHOLDER = 'your@email.com';
|
|
const PASSWORD_PLACEHOLDER = '••••••••';
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
});
|
|
|
|
describe('LoginPage', () => {
|
|
describe('FE-PAGE-LOGIN-001: Renders login form', () => {
|
|
it('shows email and password inputs', async () => {
|
|
render(<LoginPage />);
|
|
// Wait for appConfig to load (useEffect fetches it)
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-002: Submitting valid credentials triggers login', () => {
|
|
it('shows takeoff animation on successful login', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
// On success, takeoff overlay appears
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-003: Invalid credentials shows error', () => {
|
|
it('displays error message on login failure', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'bad@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'wrongpass');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
// authStore.login throws, LoginPage catches and sets error text from API response
|
|
expect(screen.getByText('Invalid credentials')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-004: Loading state while login in progress', () => {
|
|
it('disables submit button and shows spinner during login', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user' },
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
// While loading, button becomes disabled with spinner text
|
|
await waitFor(() => {
|
|
const submitBtn = screen.getByRole('button', { name: /signing in/i });
|
|
expect(submitBtn).toBeDisabled();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-005: Registration toggle visible', () => {
|
|
it('shows a Register button to switch to registration mode', async () => {
|
|
// Default appConfig has allow_registration: true, has_users: true
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
// The register toggle link text appears
|
|
expect(screen.getByRole('button', { name: /^register$/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-006: Register creates account', () => {
|
|
it('switches to register mode and submits registration form', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /^register$/i })).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(screen.getByRole('button', { name: /^register$/i }));
|
|
|
|
// Username field appears in register mode
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('admin'), 'newuser');
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'new@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
|
|
await user.click(screen.getByRole('button', { name: /create account/i }));
|
|
|
|
// On success, takeoff animation
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-007: OIDC button shown when configured', () => {
|
|
it('renders SSO sign-in link when oidc_configured is true', async () => {
|
|
server.use(
|
|
http.get('/api/auth/app-config', () => {
|
|
return HttpResponse.json({
|
|
has_users: true,
|
|
allow_registration: true,
|
|
demo_mode: false,
|
|
oidc_configured: true,
|
|
oidc_display_name: 'Okta',
|
|
oidc_only_mode: false,
|
|
oidc_login: true,
|
|
password_login: true,
|
|
password_registration: true,
|
|
setup_complete: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/sign in with okta/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-008: Demo login available in demo mode', () => {
|
|
it('shows demo button when demo_mode is true', async () => {
|
|
server.use(
|
|
http.get('/api/auth/app-config', () => {
|
|
return HttpResponse.json({
|
|
has_users: true,
|
|
allow_registration: false,
|
|
demo_mode: true,
|
|
oidc_configured: false,
|
|
oidc_only_mode: false,
|
|
setup_complete: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
// Demo hint button appears
|
|
expect(screen.getByText(/try the demo/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-009: MFA prompt after initial login', () => {
|
|
it('shows MFA code input when login returns mfa_required', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
mfa_required: true,
|
|
mfa_token: 'test-mfa-token-abc',
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
// MFA step: the title changes to "Two-factor authentication"
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/two-factor authentication/i)).toBeInTheDocument();
|
|
});
|
|
|
|
// MFA code input with correct placeholder
|
|
expect(screen.getByPlaceholderText('000000 or XXXX-XXXX')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-010: Successful login triggers navigation', () => {
|
|
it('shows takeoff overlay (navigation signal) after successful auth', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'pass1234');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
// Takeoff animation signals navigation away from login
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-011: Password change step appears when must_change_password', () => {
|
|
it('transitions to change password form when login returns must_change_password=true', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByPlaceholderText('Confirm new password')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-012: Password change form validates length', () => {
|
|
it('shows error when new password is shorter than 8 characters', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('New password'), 'short');
|
|
await user.type(screen.getByPlaceholderText('Confirm new password'), 'short');
|
|
await user.click(screen.getByRole('button', { name: /update password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/at least 8/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-013: Password change form validates mismatch', () => {
|
|
it('shows error when new passwords do not match', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('New password'), 'newpassword123');
|
|
await user.type(screen.getByPlaceholderText('Confirm new password'), 'differentpassword123');
|
|
await user.click(screen.getByRole('button', { name: /update password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/do not match/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-014: Password change success navigates', () => {
|
|
it('shows takeoff overlay after successful password change', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', must_change_password: true },
|
|
});
|
|
}),
|
|
http.put('/api/auth/me/password', () => {
|
|
return HttpResponse.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('New password')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('New password'), 'newpassword123');
|
|
await user.type(screen.getByPlaceholderText('Confirm new password'), 'newpassword123');
|
|
await user.click(screen.getByRole('button', { name: /update password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-015: First-setup mode switches to register when has_users=false', () => {
|
|
it('shows register form automatically when has_users is false', async () => {
|
|
server.use(
|
|
http.get('/api/auth/app-config', () => {
|
|
return HttpResponse.json({
|
|
has_users: false,
|
|
allow_registration: true,
|
|
demo_mode: false,
|
|
oidc_configured: false,
|
|
oidc_only_mode: false,
|
|
setup_complete: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-016: Registration disabled hides register option', () => {
|
|
it('does not show register button when allow_registration is false', async () => {
|
|
server.use(
|
|
http.get('/api/auth/app-config', () => {
|
|
return HttpResponse.json({
|
|
has_users: true,
|
|
allow_registration: false,
|
|
demo_mode: false,
|
|
oidc_configured: false,
|
|
oidc_only_mode: false,
|
|
setup_complete: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.queryByRole('button', { name: /^register$/i })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-017: OIDC-only mode hides standard login form', () => {
|
|
it('does not render email/password inputs in oidc_only_mode', async () => {
|
|
server.use(
|
|
http.get('/api/auth/app-config', () => {
|
|
return HttpResponse.json({
|
|
has_users: true,
|
|
allow_registration: false,
|
|
demo_mode: false,
|
|
oidc_configured: true,
|
|
oidc_only_mode: true,
|
|
password_login: false,
|
|
oidc_login: true,
|
|
setup_complete: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Pass noRedirect via location.state to prevent window.location.href redirect
|
|
render(<LoginPage />, {
|
|
initialEntries: [{ pathname: '/login', state: { noRedirect: true } }],
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByPlaceholderText(EMAIL_PLACEHOLDER)).toBeNull();
|
|
expect(screen.queryByPlaceholderText(PASSWORD_PLACEHOLDER)).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-018: MFA code submission completes login', () => {
|
|
it('shows takeoff overlay after successful MFA verification', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
mfa_required: true,
|
|
mfa_token: 'test-mfa-token-abc',
|
|
});
|
|
}),
|
|
http.post('/api/auth/mfa/verify-login', () => {
|
|
return HttpResponse.json({
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user' },
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('000000 or XXXX-XXXX')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('000000 or XXXX-XXXX'), '123456');
|
|
await user.click(screen.getByRole('button', { name: /verify/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.takeoff-overlay')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-019: Empty MFA code shows error', () => {
|
|
it('shows error when MFA code is empty and does not show takeoff overlay', async () => {
|
|
server.use(
|
|
http.post('/api/auth/login', () => {
|
|
return HttpResponse.json({
|
|
mfa_required: true,
|
|
mfa_token: 'test-mfa-token-abc',
|
|
});
|
|
}),
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123');
|
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('000000 or XXXX-XXXX')).toBeInTheDocument();
|
|
});
|
|
|
|
// Submit the form directly (bypasses browser constraint validation on required field)
|
|
const form = document.querySelector('form')!;
|
|
fireEvent.submit(form);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/enter the code from your authenticator/i)).toBeInTheDocument();
|
|
});
|
|
expect(document.querySelector('.takeoff-overlay')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-020: Register form validates password length', () => {
|
|
it('shows error when registration password is shorter than 8 characters', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /^register$/i })).toBeInTheDocument();
|
|
});
|
|
|
|
await user.click(screen.getByRole('button', { name: /^register$/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
|
});
|
|
|
|
await user.type(screen.getByPlaceholderText('admin'), 'newuser');
|
|
await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'new@example.com');
|
|
await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'short');
|
|
await user.click(screen.getByRole('button', { name: /create account/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/at least 8/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-LOGIN-021: Invite token pre-fills register mode', () => {
|
|
it('renders register form when invite query param is present', async () => {
|
|
server.use(
|
|
http.get('/api/auth/invite/:token', () => {
|
|
return HttpResponse.json({ valid: true });
|
|
}),
|
|
);
|
|
|
|
// Simulate ?invite=abc123 by replacing window.location.search
|
|
const originalSearch = window.location.search;
|
|
Object.defineProperty(window, 'location', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: { ...window.location, search: '?invite=abc123' },
|
|
});
|
|
|
|
render(<LoginPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('admin')).toBeInTheDocument();
|
|
});
|
|
|
|
Object.defineProperty(window, 'location', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: { ...window.location, search: originalSearch },
|
|
});
|
|
});
|
|
});
|
|
});
|