test(ui): improve auth and invitation E2E test stability

Simplify auth middleware test to use clearCookies and null session
assertion. Remove unnecessary login and navigation from session error
test. Fix InvitationsPage role selector with .or() fallback.
This commit is contained in:
alejandrobailo
2026-02-24 15:17:37 +01:00
parent 2ba2e7b5ff
commit 591405e3dc
3 changed files with 44 additions and 56 deletions
+8 -18
View File
@@ -1,6 +1,6 @@
import { test } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { TEST_CREDENTIALS } from "../helpers";
import { getSessionWithoutCookies, TEST_CREDENTIALS } from "../helpers";
import { ProvidersPage } from "../providers/providers-page";
import { ScansPage } from "../scans/scans-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
@@ -40,24 +40,14 @@ test.describe("Middleware Error Handling", () => {
await providersPage.goto();
await providersPage.verifyPageLoaded();
const cookies = await context.cookies();
const sessionCookie = cookies.find((c) =>
c.name.includes("authjs.session-token"),
);
// Remove auth cookies to simulate a broken/expired session deterministically.
await context.clearCookies();
if (sessionCookie) {
await context.clearCookies();
await context.addCookies([
{
...sessionCookie,
value: "invalid-session-token",
},
]);
const expiredSession = await getSessionWithoutCookies(page);
expect(expiredSession).toBeNull();
await scansPage.goto();
// With invalid session, should redirect to sign-in
await signInPage.verifyOnSignInPage();
}
await scansPage.goto();
await signInPage.verifyOnSignInPage();
},
);
+2 -23
View File
@@ -1,8 +1,5 @@
import { expect, test } from "@playwright/test";
import { TEST_CREDENTIALS } from "../helpers";
import { ProvidersPage } from "../providers/providers-page";
import { ScansPage } from "../scans/scans-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
test.describe("Session Error Messages", () => {
@@ -65,28 +62,10 @@ test.describe("Session Error Messages", () => {
{ tag: ["@e2e", "@auth", "@session", "@AUTH-SESSION-E2E-004"] },
async ({ page, context }) => {
const signInPage = new SignInPage(page);
const scansPage = new ScansPage(page);
const providersPage = new ProvidersPage(page);
await signInPage.loginAndVerify(TEST_CREDENTIALS.VALID);
// Navigate to a specific page (just need to be on a protected route)
await scansPage.goto();
await expect(page.locator("main")).toBeVisible();
// Navigate to a safe public page before clearing cookies
// This prevents background requests from the protected page (scans)
// triggering a client-side redirect race condition when cookies are cleared
await signInPage.goto();
// Clear cookies to simulate session expiry
await context.clearCookies();
// Try to navigate to a different protected route
// Use fresh navigation to force middleware evaluation
await providersPage.gotoFresh();
// Should be redirected to login with callbackUrl
// Navigate directly to a protected route and assert callbackUrl preservation.
await page.goto("/providers", { waitUntil: "commit" });
await signInPage.verifyRedirectWithCallback("/providers");
},
);
+34 -15
View File
@@ -1,9 +1,7 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export class InvitationsPage extends BasePage {
// Page heading
readonly pageHeadingSendInvitation: Locator;
readonly pageHeadingInvitations: Locator;
@@ -18,34 +16,52 @@ export class InvitationsPage extends BasePage {
readonly reviewInvitationDetailsButton: Locator;
readonly shareUrl: Locator;
constructor(page: Page) {
super(page);
// Page heading
this.pageHeadingInvitations = page.getByRole("heading", { name: "Invitations" });
this.pageHeadingSendInvitation = page.getByRole("heading", { name: "Send Invitation" });
this.pageHeadingInvitations = page.getByRole("heading", {
name: "Invitations",
});
this.pageHeadingSendInvitation = page.getByRole("heading", {
name: "Send Invitation",
});
// Button to invite a new user
this.inviteButton = page.getByRole("link", { name: "Send Invitation", exact: true });
this.sendInviteButton = page.getByRole("button", { name: "Send Invitation", exact: true });
this.inviteButton = page.getByRole("link", {
name: "Send Invitation",
exact: true,
});
this.sendInviteButton = page.getByRole("button", {
name: "Send Invitation",
exact: true,
});
// Form inputs
this.emailInput = page.getByRole("textbox", { name: "Email" });
// Form select
this.roleSelect = page.getByRole("combobox", { name: /Role|Select a role/i });
this.roleSelect = page
.getByRole("combobox", { name: /Role|Select a role/i })
.or(page.getByRole("button", { name: /Role|Select a role/i }))
.first();
// Form details
this.reviewInvitationDetailsButton = page.getByRole('button', { name: /Review Invitation Details/i });
this.reviewInvitationDetailsButton = page.getByRole("button", {
name: /Review Invitation Details/i,
});
// Multiple strategies to find the share URL
this.shareUrl = page.locator('a[href*="/sign-up?invitation_token="], [data-testid="share-url"], .share-url, code, pre').first();
this.shareUrl = page
.locator(
'a[href*="/sign-up?invitation_token="], [data-testid="share-url"], .share-url, code, pre',
)
.first();
}
async goto(): Promise<void> {
// Navigate to the invitations page
await super.goto("/invitations");
}
@@ -83,18 +99,21 @@ export class InvitationsPage extends BasePage {
// Select the role option
// Open the role dropdown
await expect(this.roleSelect).toBeVisible({ timeout: 15000 });
await this.roleSelect.click();
// Prefer ARIA role option inside listbox
const option = this.page.getByRole("option", { name: new RegExp(`^${role}$`, "i") });
const option = this.page.getByRole("option", {
name: new RegExp(`^${role}$`, "i"),
});
if (await option.count()) {
await option.first().click();
} else {
throw new Error(`Role option ${role} not found`);
}
// Ensure the combobox now shows the chosen value
await expect(this.roleSelect).toContainText(new RegExp(role, "i"));
// Ensure a role value was selected in the trigger
await expect(this.roleSelect).not.toContainText(/Select a role/i);
}
async verifyInviteDataPageLoaded(): Promise<void> {
@@ -108,7 +127,7 @@ export class InvitationsPage extends BasePage {
// Get the share url text content
const text = await this.shareUrl.textContent();
if (!text) {
throw new Error("Share url not found");
}