From 02e2502dcce1004aed05877f26221daf10864684 Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Sun, 21 Sep 2025 12:28:42 +0800 Subject: [PATCH] Enhance E2E Tests and Configuration (#358) * Enhance E2E Tests and Configuration - Updated `.gitignore` to exclude `.auth/` directory for cleaner test environment. - Added `test:fast` script in `package.json` for faster Playwright test execution. - Configured Playwright to include a setup project for initializing tests. - Introduced `AUTH_STATES` utility for managing authentication states in tests. - Created `auth.setup.ts` for user authentication tests, ensuring proper login flows. - Refactored various test files to utilize the new authentication state management, improving test reliability and maintainability. - Adjusted team and user billing tests to streamline setup and enhance clarity. - Refactored some dialogs --- apps/e2e/.gitignore | 1 + apps/e2e/AGENTS.md | 105 ++++++++ apps/e2e/CLAUDE.md | 105 ++++++++ apps/e2e/package.json | 8 +- apps/e2e/playwright.config.ts | 8 +- apps/e2e/tests/account/account.po.ts | 4 - apps/e2e/tests/account/account.spec.ts | 48 +++- apps/e2e/tests/admin/admin.spec.ts | 253 +++++++----------- apps/e2e/tests/auth.setup.ts | 37 +++ apps/e2e/tests/authentication/auth.po.ts | 81 +++++- apps/e2e/tests/authentication/auth.spec.ts | 31 ++- .../authentication/password-reset.spec.ts | 21 +- .../e2e/tests/invitations/invitations.spec.ts | 19 +- .../tests/team-accounts/team-accounts.po.ts | 48 ++-- .../tests/team-accounts/team-accounts.spec.ts | 45 ++-- .../team-accounts/team-invitation-mfa.spec.ts | 32 ++- .../e2e/tests/team-billing/team-billing.po.ts | 2 +- .../tests/team-billing/team-billing.spec.ts | 17 +- .../e2e/tests/user-billing/user-billing.po.ts | 12 +- .../tests/user-billing/user-billing.spec.ts | 28 +- apps/e2e/tests/utils/auth-state.ts | 10 + apps/e2e/tests/utils/stripe.po.ts | 22 +- .../members/account-members-table.tsx | 70 ++--- .../members/remove-member-dialog.tsx | 24 +- .../members/transfer-ownership-dialog.tsx | 16 +- .../members/update-member-role-dialog.tsx | 18 +- pnpm-lock.yaml | 3 + 27 files changed, 661 insertions(+), 407 deletions(-) create mode 100644 apps/e2e/AGENTS.md create mode 100644 apps/e2e/CLAUDE.md create mode 100644 apps/e2e/tests/auth.setup.ts create mode 100644 apps/e2e/tests/utils/auth-state.ts diff --git a/apps/e2e/.gitignore b/apps/e2e/.gitignore index 68c5d18f0..d19424b7b 100644 --- a/apps/e2e/.gitignore +++ b/apps/e2e/.gitignore @@ -3,3 +3,4 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +.auth diff --git a/apps/e2e/AGENTS.md b/apps/e2e/AGENTS.md new file mode 100644 index 000000000..9e2f0f8bf --- /dev/null +++ b/apps/e2e/AGENTS.md @@ -0,0 +1,105 @@ + +## End-to-End Testing with Playwright + +### Page Object Pattern (Required) + +Always use Page Objects for test organization and reusability: + +```typescript +// Example: auth.po.ts +export class AuthPageObject { + constructor(private readonly page: Page) {} + + async signIn(params: { email: string; password: string }) { + await this.page.fill('input[name="email"]', params.email); + await this.page.fill('input[name="password"]', params.password); + await this.page.click('button[type="submit"]'); + } + + async signOut() { + await this.page.click('[data-test="account-dropdown-trigger"]'); + await this.page.click('[data-test="account-dropdown-sign-out"]'); + } +} +``` + +### Reliability Patterns + +**Use `toPass()` for flaky operations** - Always wrap unreliable operations: + +```typescript +// ✅ CORRECT - Reliable email/OTP operations +await expect(async () => { + const otpCode = await this.getOtpCodeFromEmail(email); + expect(otpCode).not.toBeNull(); + await this.enterOtpCode(otpCode); +}).toPass(); + +// ✅ CORRECT - Network requests with validation +await expect(async () => { + const response = await this.page.waitForResponse(resp => + resp.url().includes('auth/v1/user') + ); + expect(response.status()).toBe(200); +}).toPass(); + +// ✅ CORRECT - Complex operations with custom intervals +await expect(async () => { + await auth.submitMFAVerification(AuthPageObject.MFA_KEY); +}).toPass({ + intervals: [500, 2500, 5000, 7500, 10_000, 15_000, 20_000] +}); +``` + +### Test Data Management + +**Email Testing**: Use `createRandomEmail()` for unique test emails: +```typescript +createRandomEmail() { + const value = Math.random() * 10000000000000; + return `${value.toFixed(0)}@makerkit.dev`; +} +``` + +**User Bootstrapping**: Use `bootstrapUser()` for consistent test user creation: +```typescript +await auth.bootstrapUser({ + email: 'test@example.com', + password: 'testingpassword', + name: 'Test User' +}); +``` + +This method creates a user with an API call. + +To sign in: + +```tsx +await auth.loginAsUser({ + email: 'test@example.com', + password: 'testingpassword', +}); +``` + +### Test Selectors + +**Always use `data-test` attributes** for reliable element selection: +```typescript +// ✅ CORRECT - Use data-test attributes +await this.page.click('[data-test="submit-button"]'); +await this.page.fill('[data-test="email-input"]', email); + +// ✅ OR +await this.page.getByTestId('submit-button').click(); + +// ❌ AVOID - Fragile selectors +await this.page.click('.btn-primary'); +await this.page.click('button:nth-child(2)'); +``` + +### Test Organization + +- **Feature-based folders**: `/tests/authentication/`, `/tests/billing/` +- **Page Objects**: `*.po.ts` files for reusable page interactions +- **Setup files**: `auth.setup.ts` for global test setup +- **Utility classes**: `/tests/utils/` for shared functionality \ No newline at end of file diff --git a/apps/e2e/CLAUDE.md b/apps/e2e/CLAUDE.md new file mode 100644 index 000000000..9e2f0f8bf --- /dev/null +++ b/apps/e2e/CLAUDE.md @@ -0,0 +1,105 @@ + +## End-to-End Testing with Playwright + +### Page Object Pattern (Required) + +Always use Page Objects for test organization and reusability: + +```typescript +// Example: auth.po.ts +export class AuthPageObject { + constructor(private readonly page: Page) {} + + async signIn(params: { email: string; password: string }) { + await this.page.fill('input[name="email"]', params.email); + await this.page.fill('input[name="password"]', params.password); + await this.page.click('button[type="submit"]'); + } + + async signOut() { + await this.page.click('[data-test="account-dropdown-trigger"]'); + await this.page.click('[data-test="account-dropdown-sign-out"]'); + } +} +``` + +### Reliability Patterns + +**Use `toPass()` for flaky operations** - Always wrap unreliable operations: + +```typescript +// ✅ CORRECT - Reliable email/OTP operations +await expect(async () => { + const otpCode = await this.getOtpCodeFromEmail(email); + expect(otpCode).not.toBeNull(); + await this.enterOtpCode(otpCode); +}).toPass(); + +// ✅ CORRECT - Network requests with validation +await expect(async () => { + const response = await this.page.waitForResponse(resp => + resp.url().includes('auth/v1/user') + ); + expect(response.status()).toBe(200); +}).toPass(); + +// ✅ CORRECT - Complex operations with custom intervals +await expect(async () => { + await auth.submitMFAVerification(AuthPageObject.MFA_KEY); +}).toPass({ + intervals: [500, 2500, 5000, 7500, 10_000, 15_000, 20_000] +}); +``` + +### Test Data Management + +**Email Testing**: Use `createRandomEmail()` for unique test emails: +```typescript +createRandomEmail() { + const value = Math.random() * 10000000000000; + return `${value.toFixed(0)}@makerkit.dev`; +} +``` + +**User Bootstrapping**: Use `bootstrapUser()` for consistent test user creation: +```typescript +await auth.bootstrapUser({ + email: 'test@example.com', + password: 'testingpassword', + name: 'Test User' +}); +``` + +This method creates a user with an API call. + +To sign in: + +```tsx +await auth.loginAsUser({ + email: 'test@example.com', + password: 'testingpassword', +}); +``` + +### Test Selectors + +**Always use `data-test` attributes** for reliable element selection: +```typescript +// ✅ CORRECT - Use data-test attributes +await this.page.click('[data-test="submit-button"]'); +await this.page.fill('[data-test="email-input"]', email); + +// ✅ OR +await this.page.getByTestId('submit-button').click(); + +// ❌ AVOID - Fragile selectors +await this.page.click('.btn-primary'); +await this.page.click('button:nth-child(2)'); +``` + +### Test Organization + +- **Feature-based folders**: `/tests/authentication/`, `/tests/billing/` +- **Page Objects**: `*.po.ts` files for reusable page interactions +- **Setup files**: `auth.setup.ts` for global test setup +- **Utility classes**: `/tests/utils/` for shared functionality \ No newline at end of file diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 1e397732f..e3621e45a 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -1,18 +1,18 @@ { "name": "web-e2e", "version": "1.0.0", - "description": "", "main": "index.js", "scripts": { "report": "playwright show-report", "test": "playwright test --max-failures=1", + "test:fast": "playwright test --max-failures=1 --workers=16", + "test:setup": "playwright test tests/auth.setup.ts", "test:ui": "playwright test --ui" }, - "keywords": [], - "author": "", - "license": "ISC", + "author": "Makerkit", "devDependencies": { "@playwright/test": "^1.55.0", + "@supabase/supabase-js": "2.57.4", "@types/node": "^24.5.2", "dotenv": "17.2.2", "node-html-parser": "^7.0.1", diff --git a/apps/e2e/playwright.config.ts b/apps/e2e/playwright.config.ts index 5e127be50..a221037cb 100644 --- a/apps/e2e/playwright.config.ts +++ b/apps/e2e/playwright.config.ts @@ -7,9 +7,10 @@ dotenvConfig({ path: '.env.local' }); /** * Number of workers to use in CI. Tweak based on your CI provider's resources. */ -const CI_WORKERS = 3; +const CI_WORKERS = 2; const enableBillingTests = process.env.ENABLE_BILLING_TESTS === 'true'; + const enableTeamAccountTests = (process.env.ENABLE_TEAM_ACCOUNT_TESTS ?? 'true') === 'true'; @@ -50,7 +51,7 @@ export default defineConfig({ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - retries: 3, + retries: 2, /* Limit parallel tests on CI. */ workers: process.env.CI ? CI_WORKERS : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ @@ -68,6 +69,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', navigationTimeout: 15 * 1000, + testIdAttribute: 'data-test', }, // test timeout set to 2 minutes timeout: 120 * 1000, @@ -77,9 +79,11 @@ export default defineConfig({ }, /* Configure projects for major browsers */ projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + dependencies: ['setup'], }, /* Test against mobile viewports. */ // { diff --git a/apps/e2e/tests/account/account.po.ts b/apps/e2e/tests/account/account.po.ts index 56b681358..15cf17b6c 100644 --- a/apps/e2e/tests/account/account.po.ts +++ b/apps/e2e/tests/account/account.po.ts @@ -14,10 +14,6 @@ export class AccountPageObject { this.otp = new OtpPo(page); } - async setup() { - return this.auth.signUpFlow('/home/settings'); - } - async updateName(name: string) { await this.page.fill('[data-test="update-account-name-form"] input', name); await this.page.click('[data-test="update-account-name-form"] button'); diff --git a/apps/e2e/tests/account/account.spec.ts b/apps/e2e/tests/account/account.spec.ts index 408aceb50..49a869955 100644 --- a/apps/e2e/tests/account/account.spec.ts +++ b/apps/e2e/tests/account/account.spec.ts @@ -1,20 +1,33 @@ -import { Page, expect, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { AuthPageObject } from '../authentication/auth.po'; import { AccountPageObject } from './account.po'; -import {AuthPageObject} from "../authentication/auth.po"; test.describe('Account Settings', () => { - let page: Page; let account: AccountPageObject; + let email: string; + + test.beforeEach(async ({ page }) => { + const auth = new AuthPageObject(page); + + email = auth.createRandomEmail(); + + auth.bootstrapUser({ + email, + password: 'testingpassword', + name: 'Test User', + }); - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); account = new AccountPageObject(page); - await account.setup(); + await auth.loginAsUser({ + email, + password: 'testingpassword', + next: '/home/settings', + }); }); - test('user can update their profile name', async () => { + test('user can update their profile name', async ({ page }) => { const name = 'John Doe'; const request = account.updateName(name); @@ -28,13 +41,13 @@ test.describe('Account Settings', () => { await expect(account.getProfileName()).toHaveText(name); }); - test('user can update their email', async () => { + test('user can update their email', async ({ page }) => { const email = account.auth.createRandomEmail(); await account.updateEmail(email); }); - test('user can update their password', async () => { + test('user can update their password', async ({ page }) => { const password = (Math.random() * 100000).toString(); const request = account.updatePassword(password); @@ -53,10 +66,19 @@ test.describe('Account Settings', () => { test.describe('Account Deletion', () => { test('user can delete their own account', async ({ page }) => { - const account = new AccountPageObject(page); + // Create a fresh user for this test since we'll be deleting it const auth = new AuthPageObject(page); + const account = new AccountPageObject(page); - const { email } = await account.setup(); + const email = auth.createRandomEmail(); + + await auth.bootstrapUser({ + email, + password: 'testingpassword', + name: 'Test User', + }); + + await auth.loginAsUser({ email, next: '/home/settings' }); await account.deleteAccount(email); @@ -70,6 +92,8 @@ test.describe('Account Deletion', () => { password: 'testingpassword', }); - await expect(page.locator('[data-test="auth-error-message"]')).toBeVisible(); + await expect( + page.locator('[data-test="auth-error-message"]'), + ).toBeVisible(); }); }); diff --git a/apps/e2e/tests/admin/admin.spec.ts b/apps/e2e/tests/admin/admin.spec.ts index 8862a39b2..09de5b22a 100644 --- a/apps/e2e/tests/admin/admin.spec.ts +++ b/apps/e2e/tests/admin/admin.spec.ts @@ -1,40 +1,23 @@ -import { Page, expect, selectors, test } from '@playwright/test'; +import { Page, expect, test } from '@playwright/test'; import { AuthPageObject } from '../authentication/auth.po'; import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po'; - -const MFA_KEY = 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE'; +import { AUTH_STATES } from '../utils/auth-state'; test.describe('Admin Auth flow without MFA', () => { + AuthPageObject.setupSession(AUTH_STATES.OWNER_USER); + test('will return a 404 for non-admin users', async ({ page }) => { - const auth = new AuthPageObject(page); - - await page.goto('/auth/sign-in'); - - await auth.signIn({ - email: 'owner@makerkit.dev', - password: 'testingpassword', - }); - - await page.waitForURL('/home'); - await page.goto('/admin'); expect(page.url()).toContain('/404'); }); +}); + +test.describe('Admin Auth flow with Super Admin but without MFA', () => { + AuthPageObject.setupSession(AUTH_STATES.TEST_USER); test('will redirect to 404 for admin users without MFA', async ({ page }) => { - const auth = new AuthPageObject(page); - - await page.goto('/auth/sign-in'); - - await auth.signIn({ - email: 'test@makerkit.dev', - password: 'testingpassword', - }); - - await page.waitForURL('/home'); - await page.goto('/admin'); expect(page.url()).toContain('/404'); @@ -42,12 +25,13 @@ test.describe('Admin Auth flow without MFA', () => { }); test.describe('Admin', () => { - // must be serial because OTP verification is not working in parallel test.describe.configure({ mode: 'serial' }); test.describe('Admin Dashboard', () => { + AuthPageObject.setupSession(AUTH_STATES.SUPER_ADMIN); + test('displays all stat cards', async ({ page }) => { - await goToAdmin(page); + await page.goto('/admin'); // Check all stat cards are present await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); @@ -73,19 +57,14 @@ test.describe('Admin', () => { }); test.describe('Personal Account Management', () => { + AuthPageObject.setupSession(AUTH_STATES.SUPER_ADMIN); + let testUserEmail: string; test.beforeEach(async ({ page }) => { - selectors.setTestIdAttribute('data-test'); - // Create a new test user before each test testUserEmail = await createUser(page); - await goToAdmin(page); - - // Navigate to the newly created user's account page - // Note: We need to get the user's ID from the email - this might need adjustment - // based on your URL structure await page.goto(`/admin/accounts`); // use the email as the filter text @@ -99,6 +78,7 @@ test.describe('Admin', () => { await expect(page.getByText('Personal Account')).toBeVisible(); await expect(page.getByTestId('admin-ban-account-button')).toBeVisible(); await expect(page.getByTestId('admin-impersonate-button')).toBeVisible(); + await expect( page.getByTestId('admin-delete-account-button'), ).toBeVisible(); @@ -106,6 +86,7 @@ test.describe('Admin', () => { test('ban user flow', async ({ page }) => { await page.getByTestId('admin-ban-account-button').click(); + await expect( page.getByRole('heading', { name: 'Ban User' }), ).toBeVisible(); @@ -189,29 +170,14 @@ test.describe('Admin', () => { const auth = new AuthPageObject(page); - await auth.signIn({ + await auth.loginAsUser({ email: testUserEmail, - password: 'testingpassword', }); - - await page.waitForURL('/home'); - }); - - test('impersonate user flow', async ({ page }) => { - await page.getByTestId('admin-impersonate-button').click(); - - await expect( - page.getByRole('heading', { name: 'Impersonate User' }), - ).toBeVisible(); - - await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM'); - await page.getByRole('button', { name: 'Impersonate User' }).click(); - - // Should redirect to home and be logged in as the user - await page.waitForURL('/home'); }); test('delete user flow', async ({ page }) => { + const auth = new AuthPageObject(page); + await page.getByTestId('admin-delete-account-button').click(); await expect( @@ -236,13 +202,10 @@ test.describe('Admin', () => { await page.waitForURL('/admin/accounts'); // Log out - await page.context().clearCookies(); + await auth.signOut(); await page.waitForURL('/'); - // Verify user can't log in - await page.goto('/auth/sign-in'); - - const auth = new AuthPageObject(page); + await auth.goToSignIn(); await auth.signIn({ email: testUserEmail, @@ -256,130 +219,116 @@ test.describe('Admin', () => { }); }); - test.describe('Team Account Management', () => { - test.skip( - process.env.ENABLE_TEAM_ACCOUNT_TESTS !== 'true', - 'Team account tests are disabled', - ); + test.describe('Impersonation', () => { + test('can sign in as a user', async ({ page }) => { + const auth = new AuthPageObject(page); - let testUserEmail: string; - let teamName: string; - let slug: string; - - test.beforeEach(async ({ page }) => { - selectors.setTestIdAttribute('data-test'); - - // Create a new test user and team account - testUserEmail = await createUser(page, { - afterSignIn: async () => { - teamName = `test-${Math.random().toString(36).substring(2, 15)}`; - - const teamAccountPo = new TeamAccountsPageObject(page); - const teamSlug = teamName.toLowerCase().replace(/ /g, '-'); - - slug = teamSlug; - - await teamAccountPo.createTeam({ - teamName, - slug, - }); - }, - }); - - await goToAdmin(page); + await auth.loginAsSuperAdmin({}); + const filterText = await createUser(page); await page.goto(`/admin/accounts`); - await filterAccounts(page, teamName); - await selectAccount(page, teamName); - }); + await filterAccounts(page, filterText); + await selectAccount(page, filterText); - test('displays team account details', async ({ page }) => { - await expect(page.getByText('Team Account')).toBeVisible(); - await expect( - page.getByTestId('admin-delete-account-button'), - ).toBeVisible(); - }); + await page.getByTestId('admin-impersonate-button').click(); - test('delete team account flow', async ({ page }) => { - await page.getByTestId('admin-delete-account-button').click(); await expect( - page.getByRole('heading', { name: 'Delete Account' }), + page.getByRole('heading', { name: 'Impersonate User' }), ).toBeVisible(); - // Try with invalid confirmation - await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG'); - await page.getByRole('button', { name: 'Delete' }).click(); - await expect( - page.getByRole('heading', { name: 'Delete Account' }), - ).toBeVisible(); // Dialog should still be open - - // Confirm with correct text await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM'); - await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Impersonate User' }).click(); - // Should redirect to admin dashboard after deletion - await expect(page).toHaveURL('/admin/accounts'); + // Should redirect to home and be logged in as the user + await page.waitForURL('/home'); }); }); }); -async function goToAdmin(page: Page) { - const auth = new AuthPageObject(page); +test.describe('Team Account Management', () => { + test.describe.configure({ mode: 'serial' }); - await page.goto('/auth/sign-in'); + test.skip( + process.env.ENABLE_TEAM_ACCOUNT_TESTS !== 'true', + 'Team account tests are disabled', + ); - await auth.signIn({ - email: 'super-admin@makerkit.dev', - password: 'testingpassword', + let testUserEmail: string; + let teamName: string; + let slug: string; + + test.beforeEach(async ({ page }) => { + const auth = new AuthPageObject(page); + + // Create a new test user and team account + testUserEmail = await createUser(page); + + teamName = `test-${Math.random().toString(36).substring(2, 15)}`; + + await auth.loginAsUser({ email: testUserEmail }); + + const teamAccountPo = new TeamAccountsPageObject(page); + const teamSlug = teamName.toLowerCase().replace(/ /g, '-'); + + slug = teamSlug; + + await teamAccountPo.createTeam({ + teamName, + slug, + }); + + await page.waitForTimeout(250); + + await auth.signOut(); + await page.waitForURL('/'); + + await auth.loginAsSuperAdmin({}); + + await page.goto(`/admin/accounts`); + + await filterAccounts(page, teamName); + await selectAccount(page, teamName); }); - await page.waitForURL('/auth/verify'); - await page.waitForTimeout(250); + test('delete team account flow', async ({ page }) => { + await expect(page.getByText('Team Account')).toBeVisible(); - await expect(async () => { - await auth.submitMFAVerification(MFA_KEY); - await page.waitForURL('/home'); - }).toPass({ - intervals: [ - 500, 2500, 5000, 7500, 10_000, 15_000, 20_000, 25_000, 30_000, 35_000, - 40_000, 45_000, 50_000, - ], + await page.getByTestId('admin-delete-account-button').click(); + + await expect( + page.getByRole('heading', { name: 'Delete Account' }), + ).toBeVisible(); + + // Try with invalid confirmation + await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG'); + await page.getByRole('button', { name: 'Delete' }).click(); + await expect( + page.getByRole('heading', { name: 'Delete Account' }), + ).toBeVisible(); // Dialog should still be open + + // Confirm with correct text + await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM'); + await page.getByRole('button', { name: 'Delete' }).click(); + + // Should redirect to admin dashboard after deletion + await expect(page).toHaveURL('/admin/accounts'); }); +}); - await page.goto('/admin'); -} - -async function createUser( - page: Page, - params: { - afterSignIn?: () => Promise; - } = {}, -) { +async function createUser(page: Page) { const auth = new AuthPageObject(page); + const password = 'testingpassword'; const email = auth.createRandomEmail(); - // sign up - await page.goto('/auth/sign-up'); - - await auth.signUp({ + // create user using bootstrap method + await auth.bootstrapUser({ email, password, - repeatPassword: password, + name: 'Test User', }); - // confirm email - await auth.visitConfirmEmailLink(email); - - if (params.afterSignIn) { - await params.afterSignIn(); - } - - // sign out - await auth.signOut(); - await page.waitForURL('/'); - // return the email return email; } @@ -404,6 +353,6 @@ async function selectAccount(page: Page, email: string) { await link.click(); - await page.waitForLoadState('networkidle'); + await page.waitForURL(/\/admin\/accounts\/[^\/]+/); }).toPass(); } diff --git a/apps/e2e/tests/auth.setup.ts b/apps/e2e/tests/auth.setup.ts new file mode 100644 index 000000000..f8df958a6 --- /dev/null +++ b/apps/e2e/tests/auth.setup.ts @@ -0,0 +1,37 @@ +import { test } from '@playwright/test'; +import { join } from 'node:path'; +import { cwd } from 'node:process'; + +import { AuthPageObject } from './authentication/auth.po'; + +const testAuthFile = join(cwd(), '.auth/test@makerkit.dev.json'); +const ownerAuthFile = join(cwd(), '.auth/owner@makerkit.dev.json'); +const superAdminAuthFile = join(cwd(), '.auth/super-admin@makerkit.dev.json'); + +test('authenticate as test user', async ({ page }) => { + const auth = new AuthPageObject(page); + + await auth.loginAsUser({ + email: 'test@makerkit.dev', + }); + + await page.context().storageState({ path: testAuthFile }); +}); + +test('authenticate as owner user', async ({ page }) => { + const auth = new AuthPageObject(page); + + await auth.loginAsUser({ + email: 'owner@makerkit.dev', + }); + + await page.context().storageState({ path: ownerAuthFile }); +}); + +test('authenticate as super-admin user', async ({ page }) => { + const auth = new AuthPageObject(page); + + await auth.loginAsSuperAdmin({}); + + await page.context().storageState({ path: superAdminAuthFile }); +}); diff --git a/apps/e2e/tests/authentication/auth.po.ts b/apps/e2e/tests/authentication/auth.po.ts index d2d080cfa..1580bf643 100644 --- a/apps/e2e/tests/authentication/auth.po.ts +++ b/apps/e2e/tests/authentication/auth.po.ts @@ -1,22 +1,33 @@ -import { Page, expect } from '@playwright/test'; +import { createClient } from '@supabase/supabase-js'; +import test, { Page, expect } from '@playwright/test'; + +import { AUTH_STATES } from '../utils/auth-state'; import { Mailbox } from '../utils/mailbox'; +const MFA_KEY = 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE'; + export class AuthPageObject { private readonly page: Page; private readonly mailbox: Mailbox; + static MFA_KEY = MFA_KEY; + constructor(page: Page) { this.page = page; this.mailbox = new Mailbox(page); } - goToSignIn() { - return this.page.goto('/auth/sign-in'); + static setupSession(user: (typeof AUTH_STATES)[keyof typeof AUTH_STATES]) { + test.use({ storageState: user }); } - goToSignUp() { - return this.page.goto('/auth/sign-up'); + goToSignIn(next?: string) { + return this.page.goto(`/auth/sign-in${next ? `?next=${next}` : ''}`); + } + + goToSignUp(next?: string) { + return this.page.goto(`/auth/sign-up${next ? `?next=${next}` : ''}`); } async signOut() { @@ -25,7 +36,7 @@ export class AuthPageObject { } async signIn(params: { email: string; password: string }) { - await this.page.waitForTimeout(500); + await this.page.waitForTimeout(100); await this.page.fill('input[name="email"]', params.email); await this.page.fill('input[name="password"]', params.password); @@ -37,7 +48,7 @@ export class AuthPageObject { password: string; repeatPassword: string; }) { - await this.page.waitForTimeout(500); + await this.page.waitForTimeout(100); await this.page.fill('input[name="email"]', params.email); await this.page.fill('input[name="password"]', params.password); @@ -108,4 +119,60 @@ export class AuthPageObject { await this.page.fill('[name="repeatPassword"]', password); await this.page.click('[type="submit"]'); } + + async loginAsSuperAdmin(params: { next?: string }) { + await this.loginAsUser({ + email: 'super-admin@makerkit.dev', + next: '/auth/verify', + }); + + // Complete MFA verification + await this.submitMFAVerification(MFA_KEY); + await this.page.waitForURL(params.next ?? '/home'); + } + + async bootstrapUser({ + email, + password, + name, + }: { + email: string; + password?: string; + name: string; + }) { + const client = createClient( + 'http://127.0.0.1:54321', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU', + ); + + const { data, error } = await client.auth.admin.createUser({ + email, + password: password || 'testingpassword', + email_confirm: true, + user_metadata: { + name, + }, + }); + + if (error) { + throw new Error(`Failed to create user: ${error.message}`); + } + + return data; + } + + async loginAsUser(params: { + email: string; + password?: string; + next?: string; + }) { + await this.goToSignIn(params.next); + + await this.signIn({ + email: params.email, + password: params.password || 'testingpassword', + }); + + await this.page.waitForURL(params.next ?? '**/home'); + } } diff --git a/apps/e2e/tests/authentication/auth.spec.ts b/apps/e2e/tests/authentication/auth.spec.ts index 53b90f18b..5ede2bbda 100644 --- a/apps/e2e/tests/authentication/auth.spec.ts +++ b/apps/e2e/tests/authentication/auth.spec.ts @@ -29,7 +29,9 @@ test.describe('Auth flow', () => { await auth.visitConfirmEmailLink(email); - await page.waitForURL('**/home'); + await page.waitForURL('**/home', { + timeout: 5_000, + }); }); test('will sign-in with the correct credentials', async ({ page }) => { @@ -43,7 +45,9 @@ test.describe('Auth flow', () => { password: 'password', }); - await page.waitForURL('**/home'); + await page.waitForURL('**/home', { + timeout: 5_000, + }); expect(page.url()).toContain('/home'); @@ -62,7 +66,9 @@ test.describe('Auth flow', () => { password: 'testingpassword', }); - await page.waitForURL('/home/settings'); + await page.waitForURL('/home/settings', { + timeout: 5_000, + }); await auth.signOut(); @@ -75,17 +81,14 @@ test.describe('Protected routes', () => { page, }) => { const auth = new AuthPageObject(page); + const path = '/home/settings'; - await page.goto('/home/settings'); + await page.goto(path); - await auth.signIn({ + await auth.loginAsUser({ email: 'test@makerkit.dev', - password: 'testingpassword', + next: path, }); - - await page.waitForURL('/home/settings'); - - expect(page.url()).toContain('/home/settings'); }); test('will redirect to the sign-in page if not authenticated', async ({ @@ -115,7 +118,9 @@ test.describe('Last auth method tracking', () => { }); await auth.visitConfirmEmailLink(testEmail); - await page.waitForURL('**/home'); + await page.waitForURL('**/home', { + timeout: 5_000, + }); // Sign out await auth.signOut(); @@ -169,7 +174,9 @@ test.describe('Last auth method tracking', () => { password: 'password123', }); - await page.waitForURL('**/home'); + await page.waitForURL('**/home', { + timeout: 5_000, + }); // Sign out and check the method is still tracked await auth.signOut(); diff --git a/apps/e2e/tests/authentication/password-reset.spec.ts b/apps/e2e/tests/authentication/password-reset.spec.ts index 908db8b46..e7cb6f95f 100644 --- a/apps/e2e/tests/authentication/password-reset.spec.ts +++ b/apps/e2e/tests/authentication/password-reset.spec.ts @@ -13,22 +13,12 @@ test.describe('Password Reset Flow', () => { await expect(async () => { email = auth.createRandomEmail(); - await page.goto('/auth/sign-up'); - - await auth.signUp({ + auth.bootstrapUser({ email, password: 'password', - repeatPassword: 'password', + name: 'Test User', }); - await auth.visitConfirmEmailLink(email, { - deleteAfter: true, - subject: 'Confirm your email', - }); - - await page.context().clearCookies(); - await page.reload(); - await page.goto('/auth/password-reset'); await page.fill('[name="email"]', email); @@ -59,13 +49,10 @@ test.describe('Password Reset Flow', () => { await page.waitForURL('/'); await page.goto('/auth/sign-in'); - await auth.signIn({ + await auth.loginAsUser({ email, password: newPassword, - }); - - await page.waitForURL('/home', { - timeout: 2000, + next: '/home', }); }); }); diff --git a/apps/e2e/tests/invitations/invitations.spec.ts b/apps/e2e/tests/invitations/invitations.spec.ts index 3d0d31bcc..ada43cde3 100644 --- a/apps/e2e/tests/invitations/invitations.spec.ts +++ b/apps/e2e/tests/invitations/invitations.spec.ts @@ -1,13 +1,11 @@ -import { Page, expect, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { InvitationsPageObject } from './invitations.po'; test.describe('Invitations', () => { - let page: Page; let invitations: InvitationsPageObject; - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); + test.beforeEach(async ({ page }) => { invitations = new InvitationsPageObject(page); await invitations.setup(); @@ -88,17 +86,12 @@ test.describe('Invitations', () => { }); test.describe('Full Invitation Flow', () => { - let page: Page; - let invitations: InvitationsPageObject; - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - invitations = new InvitationsPageObject(page); - + test('should invite users and let users accept an invite', async ({ + page, + }) => { + const invitations = new InvitationsPageObject(page); await invitations.setup(); - }); - test('should invite users and let users accept an invite', async () => { await invitations.navigateToMembers(); const invites = [ diff --git a/apps/e2e/tests/team-accounts/team-accounts.po.ts b/apps/e2e/tests/team-accounts/team-accounts.po.ts index 761238337..04db2980b 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.po.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.po.ts @@ -15,11 +15,24 @@ export class TeamAccountsPageObject { } async setup(params = this.createTeamName()) { - const { email } = await this.auth.signUpFlow('/home'); + const auth = new AuthPageObject(this.page); + + const email = auth.createRandomEmail(); + + await auth.bootstrapUser({ + email, + name: 'Test User', + }); + + await auth.loginAsUser({ email }); await this.createTeam(params); - return { email, teamName: params.teamName, slug: params.slug }; + return { + email: email, + teamName: params.teamName, + slug: params.slug, + }; } getTeamFromSelector(teamName: string) { @@ -81,11 +94,12 @@ export class TeamAccountsPageObject { async tryCreateTeam(teamName: string) { await this.page.locator('[data-test="create-team-form"] input').fill(''); await this.page.waitForTimeout(200); - await this.page.locator('[data-test="create-team-form"] input').fill(teamName); - return this.page.click( - '[data-test="create-team-form"] button:last-child', - ); + await this.page + .locator('[data-test="create-team-form"] input') + .fill(teamName); + + return this.page.click('[data-test="create-team-form"] button:last-child'); } async createTeam({ teamName, slug } = this.createTeamName()) { @@ -150,13 +164,13 @@ export class TeamAccountsPageObject { await this.page.click('[data-test="role-selector-trigger"]'); await this.page.click(`[data-test="role-option-${newRole}"]`); - // Click the confirm button - const click = this.page.click('[data-test="confirm-update-member-role"]'); - // Wait for the update to complete and page to reload - const response = this.page.waitForURL('**/home/*/members'); + const response = this.page.waitForResponse('**/members'); - return Promise.all([click, response]); + return Promise.all([ + this.page.click('[data-test="confirm-update-member-role"]'), + response, + ]); }).toPass(); } @@ -172,15 +186,13 @@ export class TeamAccountsPageObject { // Complete OTP verification await this.otp.completeOtpVerification(ownerEmail); - // Click the confirm button - const click = this.page.click( - '[data-test="confirm-transfer-ownership-button"]', - ); - // Wait for the transfer to complete and page to reload - const response = this.page.waitForURL('**/home/*/members'); + const response = this.page.waitForResponse('**/members'); - return Promise.all([click, response]); + return Promise.all([ + this.page.click('[data-test="confirm-transfer-ownership-button"]'), + response, + ]); }).toPass(); } diff --git a/apps/e2e/tests/team-accounts/team-accounts.spec.ts b/apps/e2e/tests/team-accounts/team-accounts.spec.ts index b5918dcf5..e84e6709d 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.spec.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.spec.ts @@ -56,13 +56,11 @@ async function setupTeamWithMember(page: Page, memberRole = 'member') { await page.goto('/auth/sign-in'); - await invitations.auth.signIn({ + await invitations.auth.loginAsUser({ email: ownerEmail, - password: 'password', + next: '/home', }); - await page.waitForURL('/home'); - // Navigate to the team members page await page.goto(`/home/${slug}/members`); @@ -70,17 +68,13 @@ async function setupTeamWithMember(page: Page, memberRole = 'member') { } test.describe('Team Accounts', () => { - let page: Page; - let teamAccounts: TeamAccountsPageObject; - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - teamAccounts = new TeamAccountsPageObject(page); + test.beforeEach(async ({ page }) => { + const teamAccounts = new TeamAccountsPageObject(page); + await teamAccounts.setup(); }); - test('user can update their team name (and slug)', async () => { - await teamAccounts.setup(); - + test('user can update their team name (and slug)', async ({ page }) => { + const teamAccounts = new TeamAccountsPageObject(page); const { teamName, slug } = teamAccounts.createTeamName(); await teamAccounts.goToSettings(); @@ -101,7 +95,7 @@ test.describe('Team Accounts', () => { page, }) => { const teamAccounts = new TeamAccountsPageObject(page); - await teamAccounts.setup(); + await teamAccounts.createTeam(); await teamAccounts.openAccountsSelector(); await page.click('[data-test="create-team-account-trigger"]'); @@ -132,16 +126,16 @@ test.describe('Team Accounts', () => { await teamAccounts.tryCreateTeam('Test,Name'); await expectError(); - await teamAccounts.tryCreateTeam('Test Name/') + await teamAccounts.tryCreateTeam('Test Name/'); await expectError(); - await teamAccounts.tryCreateTeam('Test Name\\') + await teamAccounts.tryCreateTeam('Test Name\\'); await expectError(); - await teamAccounts.tryCreateTeam('Test Name:') + await teamAccounts.tryCreateTeam('Test Name:'); await expectError(); - await teamAccounts.tryCreateTeam('Test Name;') + await teamAccounts.tryCreateTeam('Test Name;'); await expectError(); await teamAccounts.tryCreateTeam('Test Name='); @@ -225,14 +219,11 @@ test.describe('Team Member Role Management', () => { // Update the member's role to admin await teamAccounts.updateMemberRole(memberEmail, 'owner'); - // Wait for the page to fully load after the update - await page.waitForTimeout(1000); - - // Verify the role was updated successfully - const updatedRoleBadge = page - .getByRole('row', { name: memberEmail }) - .locator('[data-test="member-role-badge"]'); - await expect(updatedRoleBadge).toHaveText('Owner'); + await expect( + page + .getByRole('row', { name: memberEmail }) + .locator('[data-test="member-role-badge"]'), + ).toHaveText('Owner'); }); }); @@ -250,7 +241,7 @@ test.describe('Team Ownership Transfer', () => { await teamAccounts.transferOwnership(memberEmail, ownerEmail); // Wait for the page to fully load after the transfer - await page.waitForTimeout(1000); + await page.waitForTimeout(500); // Verify the transfer was successful by checking if the primary owner badge // is now on the new owner's row diff --git a/apps/e2e/tests/team-accounts/team-invitation-mfa.spec.ts b/apps/e2e/tests/team-accounts/team-invitation-mfa.spec.ts index 6b7125e61..7c9a7a570 100644 --- a/apps/e2e/tests/team-accounts/team-invitation-mfa.spec.ts +++ b/apps/e2e/tests/team-accounts/team-invitation-mfa.spec.ts @@ -4,8 +4,6 @@ import { AuthPageObject } from '../authentication/auth.po'; import { InvitationsPageObject } from '../invitations/invitations.po'; import { TeamAccountsPageObject } from './team-accounts.po'; -const MFA_KEY = 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE'; - test.describe('Team Invitation with MFA Flow', () => { test('complete flow: test@makerkit.dev creates team, invites super-admin@makerkit.dev who accepts after MFA', async ({ page, @@ -14,18 +12,12 @@ test.describe('Team Invitation with MFA Flow', () => { const teamAccounts = new TeamAccountsPageObject(page); const invitations = new InvitationsPageObject(page); - const teamName = `test-team-${Math.random().toString(36).substring(2, 15)}`; - const teamSlug = teamName.toLowerCase().replace(/ /g, '-'); - - // Step 1: test@makerkit.dev creates a team and sends invitation - await page.goto('/auth/sign-in'); - - await auth.signIn({ + await auth.loginAsUser({ email: 'test@makerkit.dev', - password: 'testingpassword', }); - await page.waitForURL('/home'); + const teamName = `test-team-${Math.random().toString(36).substring(2, 15)}`; + const teamSlug = teamName.toLowerCase().replace(/ /g, '-'); // Create a new team await teamAccounts.createTeam({ @@ -46,19 +38,24 @@ test.describe('Team Invitation with MFA Flow', () => { // Verify invitation was sent await expect(invitations.getInvitations()).toHaveCount(1); + const invitationRow = invitations.getInvitationRow( 'super-admin@makerkit.dev', ); + await expect(invitationRow).toBeVisible(); - // Sign out test@makerkit.dev - await auth.signOut(); - await page.waitForURL('/'); + await expect(async () => { + // Sign out test@makerkit.dev + await auth.signOut(); - // Step 2: super-admin@makerkit.dev signs in with MFA - await page.context().clearCookies(); + await page.waitForURL('/', { + timeout: 5_000, + }); + }).toPass(); await auth.visitConfirmEmailLink('super-admin@makerkit.dev'); + await page .locator('[data-test="existing-account-hint"]') .getByRole('link', { name: 'Already have an account?' }) @@ -71,7 +68,7 @@ test.describe('Team Invitation with MFA Flow', () => { // Complete MFA verification await expect(async () => { - await auth.submitMFAVerification(MFA_KEY); + await auth.submitMFAVerification(AuthPageObject.MFA_KEY); }).toPass({ intervals: [ 500, 2500, 5000, 7500, 10_000, 15_000, 20_000, 25_000, 30_000, 35_000, @@ -91,6 +88,7 @@ test.describe('Team Invitation with MFA Flow', () => { // Step 4: Verify membership was successful // Open account selector to verify team is available await teamAccounts.openAccountsSelector(); + const team = teamAccounts.getTeamFromSelector(teamName); await expect(team).toBeVisible(); diff --git a/apps/e2e/tests/team-billing/team-billing.po.ts b/apps/e2e/tests/team-billing/team-billing.po.ts index 463191b69..e99a75068 100644 --- a/apps/e2e/tests/team-billing/team-billing.po.ts +++ b/apps/e2e/tests/team-billing/team-billing.po.ts @@ -13,6 +13,6 @@ export class TeamBillingPageObject { } setup() { - return this.teamAccounts.setup(); + return this.teamAccounts.createTeam(); } } diff --git a/apps/e2e/tests/team-billing/team-billing.spec.ts b/apps/e2e/tests/team-billing/team-billing.spec.ts index 2b33d5cb1..a3ea2c7fd 100644 --- a/apps/e2e/tests/team-billing/team-billing.spec.ts +++ b/apps/e2e/tests/team-billing/team-billing.spec.ts @@ -1,18 +1,13 @@ -import { Page, expect, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { AuthPageObject } from '../authentication/auth.po'; import { TeamBillingPageObject } from './team-billing.po'; test.describe('Team Billing', () => { - let page: Page; - let po: TeamBillingPageObject; + test('a team can subscribe to a plan', async ({ page }) => { + const po = new TeamBillingPageObject(page); - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - po = new TeamBillingPageObject(page); - }); - - test('a team can subscribe to a plan', async () => { - await po.setup(); + await po.teamAccounts.setup(); await po.teamAccounts.goToBilling(); await po.billing.selectPlan(0); @@ -23,7 +18,7 @@ test.describe('Team Billing', () => { await po.billing.stripe.submitForm(); await expect(po.billing.successStatus()).toBeVisible({ - timeout: 25_000, + timeout: 20_000, }); await po.billing.returnToBilling(); diff --git a/apps/e2e/tests/user-billing/user-billing.po.ts b/apps/e2e/tests/user-billing/user-billing.po.ts index 941816d84..96e60bb93 100644 --- a/apps/e2e/tests/user-billing/user-billing.po.ts +++ b/apps/e2e/tests/user-billing/user-billing.po.ts @@ -1,17 +1,11 @@ import { Page } from '@playwright/test'; -import { AuthPageObject } from '../authentication/auth.po'; + import { BillingPageObject } from '../utils/billing.po'; export class UserBillingPageObject { - private readonly auth: AuthPageObject; public readonly billing: BillingPageObject; - constructor(page: Page) { - this.auth = new AuthPageObject(page); + constructor(private readonly page: Page) { this.billing = new BillingPageObject(page); } - - async setup() { - await this.auth.signUpFlow('/home/billing'); - } -} \ No newline at end of file +} diff --git a/apps/e2e/tests/user-billing/user-billing.spec.ts b/apps/e2e/tests/user-billing/user-billing.spec.ts index de604253e..4937813a6 100644 --- a/apps/e2e/tests/user-billing/user-billing.spec.ts +++ b/apps/e2e/tests/user-billing/user-billing.spec.ts @@ -1,19 +1,25 @@ -import { Page, expect, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { AuthPageObject } from '../authentication/auth.po'; import { UserBillingPageObject } from './user-billing.po'; test.describe('User Billing', () => { - let page: Page; - let po: UserBillingPageObject; - - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - po = new UserBillingPageObject(page); - - await po.setup(); - }); - test('user can subscribe to a plan', async ({ page }) => { + const po = new UserBillingPageObject(page); + const auth = new AuthPageObject(page); + + const email = auth.createRandomEmail(); + + await auth.bootstrapUser({ + email, + name: 'Test Billing User', + }); + + await auth.loginAsUser({ + email, + next: '/home/billing', + }); + await po.billing.selectPlan(0); await po.billing.proceedToCheckout(); diff --git a/apps/e2e/tests/utils/auth-state.ts b/apps/e2e/tests/utils/auth-state.ts new file mode 100644 index 000000000..cf50efe27 --- /dev/null +++ b/apps/e2e/tests/utils/auth-state.ts @@ -0,0 +1,10 @@ +import { join } from 'node:path'; +import { cwd } from 'node:process'; + +export const AUTH_STATES = { + TEST_USER: join(cwd(), '.auth/test@makerkit.dev.json'), + OWNER_USER: join(cwd(), '.auth/owner@makerkit.dev.json'), + SUPER_ADMIN: join(cwd(), '.auth/super-admin@makerkit.dev.json'), +} as const; + +export type AuthState = keyof typeof AUTH_STATES; diff --git a/apps/e2e/tests/utils/stripe.po.ts b/apps/e2e/tests/utils/stripe.po.ts index 929228736..4883b30c3 100644 --- a/apps/e2e/tests/utils/stripe.po.ts +++ b/apps/e2e/tests/utils/stripe.po.ts @@ -17,13 +17,15 @@ export class StripePageObject { }).toPass(); } - async fillForm(params: { - billingName?: string; - cardNumber?: string; - expiry?: string; - cvc?: string; - billingCountry?: string; - } = {}) { + async fillForm( + params: { + billingName?: string; + cardNumber?: string; + expiry?: string; + cvc?: string; + billingCountry?: string; + } = {}, + ) { const billingName = this.billingName(); const cardNumber = this.cardNumber(); const expiry = this.expiry(); @@ -38,7 +40,9 @@ export class StripePageObject { } submitForm() { - return this.getStripeCheckoutIframe().getByTestId('hosted-payment-submit-button').click(); + return this.getStripeCheckoutIframe() + .locator('[data-testid="hosted-payment-submit-button"]') + .click(); } cardNumber() { @@ -60,4 +64,4 @@ export class StripePageObject { billingCountry() { return this.getStripeCheckoutIframe().locator('#billingCountry'); } -} \ No newline at end of file +} diff --git a/packages/features/team-accounts/src/components/members/account-members-table.tsx b/packages/features/team-accounts/src/components/members/account-members-table.tsx index 769e57116..1fdd2f074 100644 --- a/packages/features/team-accounts/src/components/members/account-members-table.tsx +++ b/packages/features/team-accounts/src/components/members/account-members-table.tsx @@ -220,10 +220,6 @@ function ActionsDropdown({ currentTeamAccountId: string; currentRoleHierarchy: number; }) { - const [isRemoving, setIsRemoving] = useState(false); - const [isTransferring, setIsTransferring] = useState(false); - const [isUpdatingRole, setIsUpdatingRole] = useState(false); - const isCurrentUser = member.user_id === currentUserId; const isPrimaryOwner = member.primary_owner_user_id === member.user_id; @@ -258,54 +254,42 @@ function ActionsDropdown({ - setIsUpdatingRole(true)}> - - + + e.preventDefault()}> + + + - setIsTransferring(true)}> - - + + e.preventDefault()}> + + + - setIsRemoving(true)}> - - + + e.preventDefault()}> + + + - - - - - - - - - - - - ); } diff --git a/packages/features/team-accounts/src/components/members/remove-member-dialog.tsx b/packages/features/team-accounts/src/components/members/remove-member-dialog.tsx index 3c24d3eed..bce8d2c16 100644 --- a/packages/features/team-accounts/src/components/members/remove-member-dialog.tsx +++ b/packages/features/team-accounts/src/components/members/remove-member-dialog.tsx @@ -9,6 +9,7 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, } from '@kit/ui/alert-dialog'; import { Button } from '@kit/ui/button'; import { If } from '@kit/ui/if'; @@ -17,18 +18,17 @@ import { Trans } from '@kit/ui/trans'; import { removeMemberFromAccountAction } from '../../server/actions/team-members-server-actions'; export function RemoveMemberDialog({ - isOpen, - setIsOpen, teamAccountId, userId, -}: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; + children, +}: React.PropsWithChildren<{ teamAccountId: string; userId: string; -}) { +}>) { return ( - + + {children} + @@ -40,11 +40,7 @@ export function RemoveMemberDialog({ - + ); @@ -53,11 +49,9 @@ export function RemoveMemberDialog({ function RemoveMemberForm({ accountId, userId, - setIsOpen, }: { accountId: string; userId: string; - setIsOpen: (isOpen: boolean) => void; }) { const [isSubmitting, startTransition] = useTransition(); const [error, setError] = useState(); @@ -66,8 +60,6 @@ function RemoveMemberForm({ startTransition(async () => { try { await removeMemberFromAccountAction({ accountId, userId }); - - setIsOpen(false); } catch { setError(true); } diff --git a/packages/features/team-accounts/src/components/members/transfer-ownership-dialog.tsx b/packages/features/team-accounts/src/components/members/transfer-ownership-dialog.tsx index 272b1db19..fb99de31e 100644 --- a/packages/features/team-accounts/src/components/members/transfer-ownership-dialog.tsx +++ b/packages/features/team-accounts/src/components/members/transfer-ownership-dialog.tsx @@ -16,6 +16,7 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, } from '@kit/ui/alert-dialog'; import { Button } from '@kit/ui/button'; import { Form } from '@kit/ui/form'; @@ -26,20 +27,20 @@ import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-owner import { transferOwnershipAction } from '../../server/actions/team-members-server-actions'; export function TransferOwnershipDialog({ - isOpen, - setIsOpen, + children, targetDisplayName, accountId, userId, }: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; + children: React.ReactNode; accountId: string; userId: string; targetDisplayName: string; }) { return ( - + + {children} + @@ -55,7 +56,6 @@ export function TransferOwnershipDialog({ accountId={accountId} userId={userId} targetDisplayName={targetDisplayName} - setIsOpen={setIsOpen} /> @@ -66,12 +66,10 @@ function TransferOrganizationOwnershipForm({ accountId, userId, targetDisplayName, - setIsOpen, }: { userId: string; accountId: string; targetDisplayName: string; - setIsOpen: (isOpen: boolean) => void; }) { const [pending, startTransition] = useTransition(); const [error, setError] = useState(); @@ -121,8 +119,6 @@ function TransferOrganizationOwnershipForm({ startTransition(async () => { try { await transferOwnershipAction(data); - - setIsOpen(false); } catch { setError(true); } diff --git a/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx b/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx index 2d3330d5c..8ea0e12fc 100644 --- a/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx +++ b/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx @@ -12,6 +12,7 @@ import { DialogDescription, DialogHeader, DialogTitle, + DialogTrigger, } from '@kit/ui/dialog'; import { Form, @@ -33,22 +34,20 @@ import { RolesDataProvider } from './roles-data-provider'; type Role = string; export function UpdateMemberRoleDialog({ - isOpen, - setIsOpen, + children, userId, teamAccountId, userRole, userRoleHierarchy, -}: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; +}: React.PropsWithChildren<{ userId: string; teamAccountId: string; userRole: Role; userRoleHierarchy: number; -}) { +}>) { return ( - + + {children} @@ -63,7 +62,6 @@ export function UpdateMemberRoleDialog({ {(data) => ( void; roles: Role[]; }>) { const [pending, startTransition] = useTransition(); @@ -101,8 +97,6 @@ function UpdateMemberForm({ userId, role, }); - - setIsOpen(false); } catch { setError(true); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 650f38aad..8b0095a24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@playwright/test': specifier: ^1.55.0 version: 1.55.0 + '@supabase/supabase-js': + specifier: 2.57.4 + version: 2.57.4 '@types/node': specifier: ^24.5.2 version: 24.5.2