Skip to content
Open
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: 9 additions & 2 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,17 @@ jobs:
- name: Wait for Supabase to be ready ⏳
uses: ./.github/actions/wait-for-supabase

- name: Run frontend E2E test 🎭
- name: Run frontend E2E test (parallel) 🎭
run: |
cd apps/frontend
npm run test:e2e -- --project="${{ matrix.browser }}"
npm run test:e2e:parallel -- --project="${{ matrix.browser }}"

# The maintenance-mode has side effects, so it runs on its
# own (workers:1) and must not overlap the parallel suite above.
- name: Run frontend E2E test (no-parallel) 🎭
run: |
cd apps/frontend
npm run test:e2e:no-parallel -- --project="${{ matrix.browser }}"

- name: Capture backend logs after frontend E2E test after failure 📋
if: failure()
Expand Down
15 changes: 10 additions & 5 deletions apps/admin-panel/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ export default defineConfig({
expect: {
timeout: 30_000,
},
maxFailures: 1,
/* Run tests in files in parallel */
fullyParallel: false,
// No global cap: under parallel workers a single flake shouldn't abort the
// whole run. Local keeps a small cap for fast feedback.
maxFailures: process.env.CI ? undefined : 1,
/**
* Fixtures key their users/domains/emails by worker index, so tests are
* isolated and safe to run in parallel.
*/
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* No retries */
retries: 0,
/* Opt out of parallel tests. */
workers: 1,
/* Parallel workers. Local uses Playwright's default (CPU-based). */
workers: process.env.CI ? 4 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
["list"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Session } from "@supabase/supabase-js";
import { test as baseTest } from "@playwright/test";
import { supabaseAnonClient, supabaseAdminClient } from "../supabase.ts";
import { createAnonClient, supabaseAdminClient } from "../supabase.ts";
import { testWithAdminUser } from "./test-with-admin-user.ts";

type TestWithLoggedInAdminUser = {
Expand All @@ -16,7 +16,7 @@ export const testWithLoggedInAdminUser =
const { email, password } = adminAccount;

const { data, error } =
await supabaseAnonClient.auth.signInWithPassword({
await createAnonClient().auth.signInWithPassword({
email,
password,
});
Expand Down
11 changes: 11 additions & 0 deletions apps/admin-panel/tests/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,14 @@ export const supabaseAnonClient = createClient<Database>(
config.supabaseUrl,
config.supabaseAnonKey,
);

/**
* Creates a fresh, unauthenticated anon client. Sign-in flows must use their own
* instance instead of the `supabaseAnonClient`, because it can be stateful, e.g.
* logged-in with a different session.
*/
export function createAnonClient() {
return createClient<Database>(config.supabaseUrl, config.supabaseAnonKey, {
auth: { persistSession: false },
});
}
4 changes: 3 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"test": "vitest run src --passWithNoTests",
"test:watch": "vitest watch src",
"test:a11y": "npx playwright test tests/a11y --config=playwright.config.ts",
"test:e2e": "npx playwright test tests/e2e --config=playwright.config.ts"
"test:e2e": "npm run test:e2e:parallel && npm run test:e2e:no-parallel",
"test:e2e:parallel": "npx playwright test tests/e2e --grep-invert=@no-parallel --config=playwright.config.ts",
"test:e2e:no-parallel": "npx playwright test tests/e2e --grep=@no-parallel --workers=1 --config=playwright.config.ts"
},
"dependencies": {
"@e965/xlsx": "0.20.3",
Expand Down
18 changes: 14 additions & 4 deletions apps/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,25 @@ export default defineConfig({
expect: {
timeout: 30_000,
},
maxFailures: 1,
/**
* Each test provisions its own user/data, so tests are isolated and safe to
* run in parallel. The one exception — the app-global maintenance-mode toggle
* — is tagged @no-parallel and run in a separate workers:1 pass (see the
* test:e2e:parallel / test:e2e:no-parallel scripts).
*/
// No global cap: under parallel workers a single flake shouldn't abort the
// whole run. Local keeps a small cap for fast feedback.
maxFailures: process.env.CI ? undefined : 1,
/* Seed the shared "Alle" access-group documents once before the run. */
globalSetup: "./tests/global-setup.ts",
/* Run tests in files in parallel */
fullyParallel: false,
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* No retries */
retries: 0,
/* Opt out of parallel tests. */
workers: 1,
/* Parallel workers. Local uses Playwright's default (CPU-based). */
workers: process.env.CI ? 4 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
["list"],
Expand Down
20 changes: 10 additions & 10 deletions apps/frontend/tests/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import {
confirmOtp,
findUserByEmail,
testWithRegisteredUser,
} from "../fixtures/test-with-registered-user.ts";
import { supabaseAdminClient } from "../supabase.ts";
Expand Down Expand Up @@ -289,21 +290,20 @@ async function fillAndSubmitRegistrationForm(
}

test.describe("User Registration (uses different user to prevent side-effects on other tests)", () => {
const givenUserEmail = "user.registration@ts.berlin";
// Unique per test so parallel workers never register the same email or have
// one test's afterEach delete another's user. A worker runs its tests
// serially, so a single module-scoped variable set in beforeEach is safe.
let givenUserEmail: string;
const givenUserPassword = "123456789!";
const givenUserFirstName = "User";
const givenUserLastName = "Registration";

testWithoutSplashScreen.afterEach(async () => {
const { data: listUsersData, error: listUsersError } =
await supabaseAdminClient.auth.admin.listUsers();

expect(listUsersError).toBeNull();
expect(listUsersData).toBeDefined();
testWithoutSplashScreen.beforeEach(() => {
givenUserEmail = `user.registration+${crypto.randomUUID()}@ts.berlin`;
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const foundUser = listUsersData.users.find(
({ email }) => email === givenUserEmail,
);
testWithoutSplashScreen.afterEach(async () => {
const foundUser = await findUserByEmail(givenUserEmail);

expect(foundUser).toBeDefined();

Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/tests/e2e/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
secondaryDocumentType,
} from "../constants.ts";
import { testDesktopOnly } from "../fixtures/test-desktop-only.ts";
import { supabaseAdminClient, supabaseAnonClient } from "../supabase.ts";
import { createAnonClient, supabaseAdminClient } from "../supabase.ts";
import { testDesktopOnlyWithManyChats } from "../fixtures/test-desktop-only-with-many-chats.ts";
import { testWithLoggedInUser } from "../fixtures/test-with-logged-in-user.ts";

Expand Down Expand Up @@ -455,7 +455,7 @@ test.describe("Chat", () => {

testDesktopOnly("Chat with public document citations", async ({ page }) => {
// Create an admin user to upload the public document
const adminEmail = "admin.test@ts.berlin";
const adminEmail = `admin.test+${crypto.randomUUID()}@ts.berlin`;
const adminPassword = "TestPassword123!";

const { data: adminUserData, error: createAdminError } =
Expand Down Expand Up @@ -487,7 +487,7 @@ test.describe("Chat", () => {

// Sign in the admin user to get their access token
const { data: adminSessionData, error: adminSignInError } =
await supabaseAnonClient.auth.signInWithPassword({
await createAnonClient().auth.signInWithPassword({
email: adminEmail,
password: adminPassword,
});
Expand Down
28 changes: 25 additions & 3 deletions apps/frontend/tests/e2e/maintenance-mode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { createClient } from "@supabase/supabase-js";
import type { Database } from "@repo/db-schema";
import { expect, test } from "@playwright/test";
import { testWithLoggedInUser } from "../fixtures/test-with-logged-in-user.ts";
import { supabaseAdminClient, supabaseAnonClient } from "../supabase.ts";
import { config } from "../config.ts";
import { defaultUserFirstName, defaultUserLastName } from "../constants.ts";

test.describe("Maintenance Mode", () => {
// Tagged @no-parallel: these tests flip the app-global `maintenance_mode` row,
// which logs out every user app-wide. The CI/npm scripts run this describe on
// its own (workers:1), separately from the parallel suite, so it never overlaps
// other tests. See playwright.config / package.json test:e2e:no-parallel.
test.describe("Maintenance Mode", { tag: "@no-parallel" }, () => {
test.beforeEach(async () => {
// Ensure maintenance mode is disabled before each test
// Use upsert since we can't delete from the table
Expand Down Expand Up @@ -82,8 +89,23 @@ test.describe("Maintenance Mode", () => {

testWithLoggedInUser(
"authenticated user cannot update maintenance_mode",
async () => {
const { error } = await supabaseAnonClient
async ({ session }) => {
// Build a client carrying the logged-in (non-admin) user's JWT. The
// shared `supabaseAnonClient` is no longer authenticated by the login
// fixture (sign-in uses a per-test client), so we must attach the
// session explicitly to actually exercise the authenticated path.
const authedClient = createClient<Database>(
config.supabaseUrl,
config.supabaseAnonKey,
{
global: {
headers: { Authorization: `Bearer ${session.access_token}` },
},
auth: { persistSession: false },
},
);

const { error } = await authedClient
.from("maintenance_mode")
.upsert(
{ onerow_id: true, is_enabled: true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Session } from "@supabase/supabase-js";
import { supabaseAnonClient } from "../supabase.ts";
import { supabaseAdminClient } from "../supabase.ts";
import { expect } from "@playwright/test";
import { testDesktopOnly } from "./test-desktop-only.ts";

Expand Down Expand Up @@ -32,7 +32,7 @@ async function addChatsToAccount(session: Session, numberOfChats: number) {
created_at: new Date(Date.now() - (numberOfChats - 1 - i)).toISOString(),
}));

const { error } = await supabaseAnonClient.from("chats").insert([...chats]);
const { error } = await supabaseAdminClient.from("chats").insert([...chats]);

expect(error).toBeNull();
}
10 changes: 5 additions & 5 deletions apps/frontend/tests/fixtures/test-with-chat-search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from "@playwright/test";
import type { Session } from "@supabase/supabase-js";
import { testDesktopOnly } from "./test-desktop-only.ts";
import { supabaseAnonClient } from "../supabase.ts";
import { supabaseAdminClient } from "../supabase.ts";

/** Matches `RANGE_LIMIT + 1` in `src/api/chat/get-chats.ts`. */
export const CHATS_PAGE_SIZE = 20;
Expand All @@ -27,12 +27,12 @@ type TestWithChatSearch = {

/**
* Extends `testDesktopOnly` with helpers that seed chats/messages via
* `supabaseAnonClient` for the current test user.
* `supabaseAdminClient` for the current test user.
*/
export const testWithChatSearch = testDesktopOnly.extend<TestWithChatSearch>({
insertChat: async ({ session }, use) => {
await use(async (name, createdAt) => {
const { data, error } = await supabaseAnonClient
const { data, error } = await supabaseAdminClient
.from("chats")
.insert({
user_id: session.user.id,
Expand All @@ -54,7 +54,7 @@ export const testWithChatSearch = testDesktopOnly.extend<TestWithChatSearch>({

insertMessages: async ({}, use) => {
await use(async (chatId, messages) => {
const { data, error } = await supabaseAnonClient
const { data, error } = await supabaseAdminClient
.from("chat_messages")
.insert(
messages.map(({ role, content, createdAt }) => ({
Expand Down Expand Up @@ -133,7 +133,7 @@ export async function insertFillerChats(
};
});

const { error } = await supabaseAnonClient.from("chats").insert(chats);
const { error } = await supabaseAdminClient.from("chats").insert(chats);

expect(error, error?.message).toBeNull();
}
Loading