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
34 changes: 30 additions & 4 deletions packages/frontend/e2e/helpers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,43 @@ export const emailVerifyPage = {
} as const;

/**
* Performs a complete login flow: navigates from the method
* selection page through the password form and submits credentials.
* Moves from the current login entry point to the password form.
* Password-only configs redirect from /login directly to /login/password,
* while multi-method configs still require selecting the password method.
*/
export async function openPasswordLoginFromCurrentPage(
page: Page,
): Promise<void> {
const passwordForm = page.locator(loginPasswordPage.emailInput);
const passwordMethodLink = page.locator(loginMethodPage.passwordMethodLink);

for (let attempt = 0; attempt < 50; attempt += 1) {
if (await passwordForm.isVisible()) {
return;
}

if (await passwordMethodLink.isVisible()) {
await passwordMethodLink.click();
await page.waitForURL('**/login/password**');
return;
}

await page.waitForTimeout(100);
}

await passwordForm.waitFor({ state: 'visible', timeout: 1_000 });
}

/**
* Performs a complete login flow through the password form.
*/
export async function performLogin(
page: Page,
email: string,
password: string,
): Promise<void> {
await gotoLogin(page);
await page.locator(loginMethodPage.passwordMethodLink).click();
await page.waitForURL('**/login/password');
await openPasswordLoginFromCurrentPage(page);
await page.locator(loginPasswordPage.emailInput).fill(email);
await page.locator(loginPasswordPage.passwordInput).fill(password);
await page.locator(loginPasswordPage.submitButton).click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ test.describe('Delete account flow', () => {
await page.locator(deleteAccountModal.submitButton).click();

// Should redirect to login
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

Expand Down Expand Up @@ -158,7 +158,7 @@ test.describe('Delete account flow', () => {
await page.locator(deleteAccountModal.submitButton).click();

// Wait for redirect to login
await page.waitForURL('**/login');
await page.waitForURL('**/login**');

// Try to login with the deleted account
await gotoWithFirefoxRetry(page, browserName, '/login/password');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test.describe('Forgot password flow', () => {
await expect(loginLink).toBeVisible();
await loginLink.click();

await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ test.describe('Reset password flow', () => {
await page.getByRole('button', { name: 'Go to login' }).click();

// Should navigate to login
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ test.describe('OAuth continuation across email verification and 2FA', () => {
hasVirtualAuth = false;

await page.getByRole('button', { name: 'Log out' }).click();
await page.waitForURL('**/login');
await page.waitForURL('**/login**');

await page.goto(buildAuthEntryUrl('login', 'password', oauthParams));
await submitPasswordLogin(page, email, TEST_PASSWORD);
Expand Down
10 changes: 4 additions & 6 deletions packages/frontend/e2e/tests/minimal/account-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
E2E_TEST_CLIENT_CONFIG,
} from '#frontend-e2e/fixtures/index.ts';
import { consentPage } from '#frontend-e2e/helpers/consent.ts';
import { openPasswordLoginFromCurrentPage } from '#frontend-e2e/helpers/login.ts';
import {
buildAuthorizePath,
buildOAuthFlowInput,
Expand Down Expand Up @@ -272,8 +273,7 @@ test.describe('OIDC account selection', () => {

await page.getByRole('link', { name: /Use another account/i }).click();
await expect(page).toHaveURL(/\/login/);
await page.locator('a[href^="/login/password"]').click();
await expect(page).toHaveURL(/\/login\/password/);
await openPasswordLoginFromCurrentPage(page);
await page
.locator('input[name="email"]')
.fill('account-selection-carol@example.com');
Expand Down Expand Up @@ -460,8 +460,7 @@ test.describe('OIDC account selection', () => {
buildAuthorizePath(flow.authorizeParams),
);
await expect(page).toHaveURL(/\/login/);
await page.locator('a[href^="/login/password"]').click();
await expect(page).toHaveURL(/\/login\/password/);
await openPasswordLoginFromCurrentPage(page);

const callbackRoute = `${E2E_TEST_CLIENT.redirectUri}**`;
const callbackRouteHandler = async (
Expand Down Expand Up @@ -531,8 +530,7 @@ test.describe('OIDC account selection', () => {

await expect(page).toHaveURL(/\/login/);
expect(page.url()).not.toContain('/account/select');
await page.locator('a[href^="/login/password"]').click();
await expect(page).toHaveURL(/\/login\/password/);
await openPasswordLoginFromCurrentPage(page);

const callbackRoute = `${E2E_TEST_CLIENT.redirectUri}**`;
const callbackRouteHandler = async (
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/e2e/tests/minimal/error-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test.describe('Error page', () => {
// Click "Go to login" link
await page.getByRole('link', { name: 'Go to login' }).click();

await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});
});
22 changes: 9 additions & 13 deletions packages/frontend/e2e/tests/minimal/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
E2E_TEST_USER_CONFIG,
} from '#frontend-e2e/fixtures/index.ts';
import {
loginMethodPage,
loginPasswordPage,
performLogin,
} from '#frontend-e2e/helpers/login.ts';
Expand All @@ -21,23 +20,22 @@ const test = createScenarioFixture((backendPort) => ({
test.describe('Login flow', () => {
test('redirects unauthenticated users to /login', async ({ page }) => {
await page.goto('/');
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

test('method selection page shows password login link', async ({ page }) => {
test('password-only login opens the password form directly', async ({
page,
}) => {
await page.goto('/login');
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await page.waitForURL('**/login/password**');
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
});

test('clicking password method navigates to password form', async ({
test('password login form is available at the direct URL', async ({
page,
}) => {
await page.goto('/login');
await page.locator(loginMethodPage.passwordMethodLink).click();
await page.waitForURL('**/login/password');
await page.goto('/login/password');

await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
await expect(page.locator(loginPasswordPage.passwordInput)).toBeVisible();
Expand Down Expand Up @@ -110,9 +108,7 @@ test.describe('Login flow', () => {
await expect(page).toHaveURL(/\/login\/password/);
});

test('full flow: method selection through login to profile', async ({
page,
}) => {
test('full password login flow navigates to profile', async ({ page }) => {
await performLogin(page, E2E_TEST_USER.email, E2E_TEST_USER.password);
await page.waitForURL('**/profile');
await expect(page).toHaveURL(/\/profile/);
Expand Down
35 changes: 11 additions & 24 deletions packages/frontend/e2e/tests/minimal/oauth-client-auth-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
import { consentPage } from '#frontend-e2e/helpers/consent.ts';
import { uniqueEmail as createUniqueEmail } from '#frontend-e2e/helpers/identity.ts';
import {
loginMethodPage,
loginPasswordPage,
openPasswordLoginFromCurrentPage,
} from '#frontend-e2e/helpers/login.ts';
import {
allowConsentAndCaptureCode,
Expand Down Expand Up @@ -61,8 +61,7 @@ async function loginThroughPasswordForm(
email: string,
password: string,
): Promise<void> {
await page.locator(loginMethodPage.passwordMethodLink).click();
await page.waitForURL('**/login/password**');
await openPasswordLoginFromCurrentPage(page);
await page.locator(loginPasswordPage.emailInput).fill(email);
await page.locator(loginPasswordPage.passwordInput).fill(password);
await page.locator(loginPasswordPage.submitButton).click();
Expand All @@ -78,9 +77,8 @@ test.describe('OAuth client authentication flow', () => {

await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
await expect(page.getByTestId('authorization-context')).toBeVisible();
expectOAuthParamsInCurrentUrl(page, oauth.authorizeParams);
});

Expand All @@ -96,9 +94,7 @@ test.describe('OAuth client authentication flow', () => {
await page.goto(buildAuthorizePath(oauth.authorizeParams));

await page.waitForURL('**/login**');
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
expectOAuthParamsInCurrentUrl(page, oauth.authorizeParams);

await loginThroughPasswordForm(page, email, TEST_PASSWORD);
Expand Down Expand Up @@ -128,9 +124,7 @@ test.describe('OAuth client authentication flow', () => {
await page.goto(buildAuthorizePath(oauth.authorizeParams));

await page.waitForURL('**/login**');
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
expectOAuthParamsInCurrentUrl(page, oauth.authorizeParams);

await page.goto(
Expand Down Expand Up @@ -172,9 +166,7 @@ test.describe('OAuth client authentication flow', () => {
`${String(baseURL)}${buildAuthorizePath(initialFlow.authorizeParams)}`,
);
await firstPage.waitForURL('**/login**');
await expect(
firstPage.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(firstPage.locator(loginPasswordPage.emailInput)).toBeVisible();
await loginThroughPasswordForm(firstPage, email, TEST_PASSWORD);
await firstPage.waitForURL('**/consent**');

Expand All @@ -196,11 +188,10 @@ test.describe('OAuth client authentication flow', () => {
);
await secondPage.waitForURL('**/login**');
await expect(
secondPage.locator(loginMethodPage.passwordMethodLink),
secondPage.locator(loginPasswordPage.emailInput),
).toBeVisible();

await secondPage.locator(loginMethodPage.passwordMethodLink).click();
await secondPage.waitForURL('**/login/password**');
await openPasswordLoginFromCurrentPage(secondPage);
await secondPage.locator(loginPasswordPage.emailInput).fill(email);
await secondPage
.locator(loginPasswordPage.passwordInput)
Expand Down Expand Up @@ -245,9 +236,7 @@ test.describe('OAuth client authentication flow', () => {
await page.goto(buildAuthorizePath(oauth.authorizeParams));

await page.waitForURL('**/login**');
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
await loginThroughPasswordForm(page, email, TEST_PASSWORD);

await page.waitForURL('**/consent**');
Expand All @@ -268,9 +257,7 @@ test.describe('OAuth client authentication flow', () => {
await page.goto(buildAuthorizePath(oauth.authorizeParams));

await page.waitForURL('**/login**');
await expect(
page.locator(loginMethodPage.passwordMethodLink),
).toBeVisible();
await expect(page.locator(loginPasswordPage.emailInput)).toBeVisible();
await loginThroughPasswordForm(page, email, TEST_PASSWORD);
await page.waitForURL('**/consent**');

Expand Down
6 changes: 3 additions & 3 deletions packages/frontend/e2e/tests/minimal/profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ test.describe('Profile page', () => {
await page.getByRole('button', { name: 'Log out' }).click();

// Should redirect to login
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

test('unauthenticated access redirects to login', async ({ page }) => {
await page.goto('/profile');
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});
});
Expand Down Expand Up @@ -169,7 +169,7 @@ test.describe('Change password', () => {
await expect(page.locator(modal.openModal)).not.toBeVisible();

await page.getByRole('button', { name: 'Log out' }).click();
await page.waitForURL('**/login');
await page.waitForURL('**/login**');

await expectPasswordLoginRejected(page, browserName, email, TEST_PASSWORD);
await expectPasswordLoginSucceeds(page, browserName, email, NEW_PASSWORD);
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/e2e/tests/minimal/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ test.describe('Registration flow (minimal config)', () => {
await page.goto('/register');
await page.locator(registerPage.loginLink).click();

await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const TEST_PASSWORD = 'test-password-123';

async function logoutFromProfile(page: Page): Promise<void> {
await page.getByRole('button', { name: 'Log out' }).click();
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
}

async function loginWithPasskeyFromMethodPage(page: Page): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function registerPasskeyFromRequiredSetup(
async function logoutFromProfile(page: Page): Promise<void> {
await expect(page.getByRole('button', { name: 'Log out' })).toBeVisible();
await page.getByRole('button', { name: 'Log out' }).click();
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
}

const TEST_PASSWORD = 'test-password-123';
Expand Down Expand Up @@ -113,7 +113,7 @@ test.describe('Passkey-required flow', () => {
await page.waitForURL('**/profile');

await page.getByRole('button', { name: 'Log out' }).click();
await page.waitForURL('**/login');
await page.waitForURL('**/login**');

const loginRes = await page.request.post(
`${String(baseURL)}/api/auth/login`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ test.describe('Password disabled configuration', () => {
browserName,
}) => {
await gotoWithFirefoxRetry(page, browserName, '/password/forgot');
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);

await gotoWithFirefoxRetry(
page,
browserName,
'/password/reset?token=test-token',
);
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

Expand All @@ -70,7 +70,7 @@ test.describe('Password disabled configuration', () => {
browserName,
'/verify/email?token=test-token',
);
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test.describe('Registration disabled', () => {
await page.goto('/register');

// beforeLoad guard checks public_registration: false and redirects
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ test.describe('Terms complete registration mode', () => {
page,
}) => {
await page.goto('/terms');
await page.waitForURL('**/login');
await page.waitForURL('**/login**');
await expect(page).toHaveURL(/\/login/);
});

Expand Down
Loading