feat: generate simple auth tests (#8709)

This commit is contained in:
Nicholas Tindle
2024-11-20 09:21:16 -06:00
committed by GitHub
parent cf43248ab8
commit 92bfbfad57
12 changed files with 237 additions and 6 deletions

View File

@@ -69,6 +69,10 @@ jobs:
run: |
cp ../supabase/docker/.env.example ../.env
- name: Copy backend .env
run: |
cp ../backend/.env.example ../backend/.env
- name: Run docker compose
run: |
docker compose -f ../docker-compose.yml up -d

View File

@@ -23,6 +23,7 @@
"defaults"
],
"dependencies": {
"@faker-js/faker": "^9.2.0",
"@hookform/resolvers": "^3.9.1",
"@next/third-parties": "^15.0.3",
"@radix-ui/react-avatar": "^1.1.1",

View File

@@ -4,10 +4,10 @@ import { defineConfig, devices } from "@playwright/test";
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
import dotenv from "dotenv";
import path from "path";
dotenv.config({ path: path.resolve(__dirname, ".env") });
dotenv.config({ path: path.resolve(__dirname, "../backend/.env") });
/**
* See https://playwright.dev/docs/test-configuration.
*/

View File

@@ -0,0 +1,46 @@
import { test, expect } from "./fixtures";
test.describe("Authentication", () => {
test("user can login successfully", async ({ page, loginPage, testUser }) => {
await page.goto("/login"); // Make sure we're on the login page
await loginPage.login(testUser.email, testUser.password);
// expect to be redirected to the home page
await expect(page).toHaveURL("/");
// expect to see the Monitor text
await expect(page.getByText("Monitor")).toBeVisible();
});
test("user can logout successfully", async ({
page,
loginPage,
testUser,
}) => {
await page.goto("/login"); // Make sure we're on the login page
await loginPage.login(testUser.email, testUser.password);
// Expect to be on the home page
await expect(page).toHaveURL("/");
// Click on the user menu
await page.getByRole("button", { name: "CN" }).click();
// Click on the logout menu item
await page.getByRole("menuitem", { name: "Log out" }).click();
// Expect to be redirected to the login page
await expect(page).toHaveURL("/login");
});
test("login in, then out, then in again", async ({
page,
loginPage,
testUser,
}) => {
await page.goto("/login"); // Make sure we're on the login page
await loginPage.login(testUser.email, testUser.password);
await page.goto("/");
await page.getByRole("button", { name: "CN" }).click();
await page.getByRole("menuitem", { name: "Log out" }).click();
await expect(page).toHaveURL("/login");
await loginPage.login(testUser.email, testUser.password);
await expect(page).toHaveURL("/");
await expect(page.getByText("Monitor")).toBeVisible();
});
});

View File

@@ -0,0 +1,18 @@
import { test as base } from "@playwright/test";
import { createTestUserFixture } from "./test-user.fixture";
import { createLoginPageFixture } from "./login-page.fixture";
import type { TestUser } from "./test-user.fixture";
import { LoginPage } from "../pages/login.page";
type Fixtures = {
testUser: TestUser;
loginPage: LoginPage;
};
// Combine fixtures
export const test = base.extend<Fixtures>({
testUser: createTestUserFixture,
loginPage: createLoginPageFixture,
});
export { expect } from "@playwright/test";

View File

@@ -0,0 +1,14 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
export const loginPageFixture = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
// Export just the fixture function
export const createLoginPageFixture = async ({ page }, use) => {
await use(new LoginPage(page));
};

View File

@@ -0,0 +1,83 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { faker } from "@faker-js/faker";
export type TestUser = {
email: string;
password: string;
id?: string;
};
let supabase: SupabaseClient;
function getSupabaseAdmin() {
if (!supabase) {
supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
}
return supabase;
}
async function createTestUser(userData: TestUser): Promise<TestUser> {
const supabase = getSupabaseAdmin();
const { data: authUser, error: authError } = await supabase.auth.signUp({
email: userData.email,
password: userData.password,
});
if (authError) {
throw new Error(`Failed to create test user: ${authError.message}`);
}
return {
...userData,
id: authUser.user?.id,
};
}
async function deleteTestUser(userId: string) {
const supabase = getSupabaseAdmin();
try {
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) {
console.warn(`Warning: Failed to delete test user: ${error.message}`);
}
} catch (error) {
console.warn(
`Warning: Error during user cleanup: ${(error as Error).message}`,
);
}
}
function generateUserData(): TestUser {
return {
email: `test.${faker.string.uuid()}@example.com`,
password: faker.internet.password({ length: 12 }),
};
}
// Export just the fixture function
export const createTestUserFixture = async ({}, use) => {
let user: TestUser | null = null;
try {
const userData = generateUserData();
user = await createTestUser(userData);
await use(user);
} finally {
if (user?.id) {
await deleteTestUser(user.id);
}
}
};

View File

@@ -0,0 +1,51 @@
import { Page } from "@playwright/test";
export class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
console.log("Attempting login with:", { email, password }); // Debug log
// Fill email
const emailInput = this.page.getByPlaceholder("user@email.com");
await emailInput.waitFor({ state: "visible" });
await emailInput.fill(email);
// Fill password
const passwordInput = this.page.getByPlaceholder("password");
await passwordInput.waitFor({ state: "visible" });
await passwordInput.fill(password);
// Check terms
const termsCheckbox = this.page.getByLabel("I agree to the Terms of Use");
await termsCheckbox.waitFor({ state: "visible" });
await termsCheckbox.click();
// TODO: This is a workaround to wait for the page to load after filling the email and password
const emailInput2 = this.page.getByPlaceholder("user@email.com");
await emailInput2.waitFor({ state: "visible" });
await emailInput2.fill(email);
// Fill password
const passwordInput2 = this.page.getByPlaceholder("password");
await passwordInput2.waitFor({ state: "visible" });
await passwordInput2.fill(password);
// Wait for the button to be ready
const loginButton = this.page.getByRole("button", { name: "Log in" });
await loginButton.waitFor({ state: "visible" });
// Start waiting for navigation before clicking
const navigationPromise = this.page.waitForURL("/", { timeout: 60000 });
console.log("About to click login button"); // Debug log
await loginButton.click();
console.log("Waiting for navigation"); // Debug log
await navigationPromise;
console.log("Navigation complete, waiting for network idle"); // Debug log
await this.page.waitForLoadState("networkidle", { timeout: 60000 });
console.log("Login process complete"); // Debug log
}
}

View File

@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "./fixtures";
test("has title", async ({ page }) => {
await page.goto("/");

View File

@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "./fixtures";
import { setNestedProperty } from "../lib/utils";
const testCases = [

View File

@@ -0,0 +1,9 @@
import { faker } from "@faker-js/faker";
export function generateUser() {
return {
email: faker.internet.email(),
password: faker.internet.password(),
name: faker.person.fullName(),
};
}

View File

@@ -1202,6 +1202,11 @@
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
"@faker-js/faker@^9.2.0":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.2.0.tgz#269ee3a5d2442e88e10d984e106028422bcb9551"
integrity sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==
"@floating-ui/core@^1.6.0":
version "1.6.7"
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz"