From 79f48b1db940aed26d79729f63bf66a188c89082 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 30 Oct 2025 15:35:30 +0530 Subject: [PATCH 01/19] fix: error message! --- .../auth/user-[user]/updateStatus.svelte | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte index ffbd35e642..88e389ee89 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte @@ -18,20 +18,21 @@ async function updateVerificationEmail() { showVerificationDropdown = false; try { - await sdk + const userUpdated = await sdk .forProject(page.params.region, page.params.project) .users.updateEmailVerification({ userId: $user.$id, emailVerification: !$user.emailVerification }); - await invalidate(Dependencies.USER); + addNotification({ - message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${ - !$user.emailVerification ? 'unverified' : 'verified' + message: `${userUpdated.name || userUpdated.email || userUpdated.phone || 'The account'} has been ${ + !userUpdated.emailVerification ? 'unverified' : 'verified' }`, type: 'success' }); trackEvent(Submit.UserUpdateVerificationEmail); + await invalidate(Dependencies.USER); } catch (error) { addNotification({ message: error.message, @@ -43,19 +44,20 @@ async function updateVerificationPhone() { showVerificationDropdown = false; try { - await sdk + const userUpdated = await sdk .forProject(page.params.region, page.params.project) .users.updatePhoneVerification({ userId: $user.$id, phoneVerification: !$user.phoneVerification }); - await invalidate(Dependencies.USER); + addNotification({ - message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${ - $user.phoneVerification ? 'unverified' : 'verified' + message: `${userUpdated.name || userUpdated.email || userUpdated.phone || 'The account'} has been ${ + !userUpdated.phoneVerification ? 'unverified' : 'verified' }`, type: 'success' }); + await invalidate(Dependencies.USER); trackEvent(Submit.UserUpdateVerificationPhone); } catch (error) { addNotification({ From 8680f41d6db7ed479b2e5f07b759eb453c45463f Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 30 Oct 2025 15:36:30 +0530 Subject: [PATCH 02/19] add: phase 1 auth tests. --- e2e/auth/navigation.ts | 79 +++++++ e2e/auth/users.ts | 407 +++++++++++++++++++++++++++++++++ e2e/journeys/auth-free.spec.ts | 134 +++++++++++ 3 files changed, 620 insertions(+) create mode 100644 e2e/auth/navigation.ts create mode 100644 e2e/auth/users.ts create mode 100644 e2e/journeys/auth-free.spec.ts diff --git a/e2e/auth/navigation.ts b/e2e/auth/navigation.ts new file mode 100644 index 0000000000..21f4de519b --- /dev/null +++ b/e2e/auth/navigation.ts @@ -0,0 +1,79 @@ +import { test, type Page } from '@playwright/test'; + +export function buildAuthUrl(region: string, projectId: string, path: string = ''): string { + return `./project-${region}-${projectId}/auth${path}`; +} + +export function buildAuthUrlPattern(region: string, projectId: string, path: string = ''): RegExp { + return new RegExp(`/project-${region}-${projectId}/auth${path}`); +} + +export async function navigateToUsers( + page: Page, + region: string, + projectId: string +): Promise { + return test.step('navigate to users', async () => { + await page.goto(buildAuthUrl(region, projectId)); + await page.waitForURL(buildAuthUrlPattern(region, projectId)); + await page.waitForLoadState('domcontentloaded'); + }); +} + +export async function navigateToUser( + page: Page, + region: string, + projectId: string, + userId: string +): Promise { + return test.step(`navigate to user ${userId}`, async () => { + await page.goto(buildAuthUrl(region, projectId, `/user-${userId}`)); + await page.waitForURL(buildAuthUrlPattern(region, projectId, `/user-${userId}`)); + await page.waitForLoadState('domcontentloaded'); + }); +} + +export async function navigateToTeams( + page: Page, + region: string, + projectId: string +): Promise { + return test.step('navigate to teams', async () => { + await page.goto(buildAuthUrl(region, projectId, '/teams')); + await page.waitForURL(buildAuthUrlPattern(region, projectId, '/teams')); + }); +} + +export async function navigateToTeam( + page: Page, + region: string, + projectId: string, + teamId: string +): Promise { + return test.step(`navigate to team ${teamId}`, async () => { + await page.goto(buildAuthUrl(region, projectId, `/teams/team-${teamId}`)); + await page.waitForURL(buildAuthUrlPattern(region, projectId, `/teams/team-${teamId}`)); + }); +} + +export async function navigateToSecurity( + page: Page, + region: string, + projectId: string +): Promise { + return test.step('navigate to security settings', async () => { + await page.goto(buildAuthUrl(region, projectId, '/security')); + await page.waitForURL(buildAuthUrlPattern(region, projectId, '/security')); + }); +} + +export async function navigateToTemplates( + page: Page, + region: string, + projectId: string +): Promise { + return test.step('navigate to templates', async () => { + await page.goto(buildAuthUrl(region, projectId, '/templates')); + await page.waitForURL(buildAuthUrlPattern(region, projectId, '/templates')); + }); +} diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts new file mode 100644 index 0000000000..cf755664a1 --- /dev/null +++ b/e2e/auth/users.ts @@ -0,0 +1,407 @@ +import { test, expect, type Page } from '@playwright/test'; +import { navigateToUsers, navigateToUser, buildAuthUrlPattern } from './navigation'; + +export type CreateUserOptions = { + name?: string; + email?: string; + phone?: string; + password?: string; + userId?: string; +}; + +export type UserMetadata = { + id: string; + name?: string; + email?: string; + phone?: string; +}; + +export type UserPrefs = { + [key: string]: string; +}; + +export async function createUser( + page: Page, + region: string, + projectId: string, + options: CreateUserOptions = {} +): Promise { + return test.step('create user', async () => { + await navigateToUsers(page, region, projectId); + + await page.getByRole('button', { name: 'Create user' }).first().click(); + + const modal = page.locator('dialog[open]').filter({ hasText: 'Create user' }); + await modal.waitFor({ state: 'visible' }); + + if (options.name) { + await modal.locator('id=name').fill(options.name); + } + + if (options.email) { + await modal.locator('id=email').fill(options.email); + } + + if (options.phone) { + await modal.locator('id=phone').fill(options.phone); + } + + if (options.password) { + await modal.locator('id=password').fill(options.password); + } + + if (options.userId) { + await modal.getByText('User ID').click(); + await modal.locator('id=id').fill(options.userId); + } + + await modal.getByRole('button', { name: 'Create', exact: true }).click(); + await modal.waitFor({ state: 'hidden' }); + await expect(page.getByText(/has been created/i)).toBeVisible(); + await page.waitForURL(/\/auth\/user-[^/]+$/); + + const currentUrl = page.url(); + const userIdMatch = currentUrl.match(/\/auth\/user-([^/]+)/); + const userId = userIdMatch ? userIdMatch[1] : options.userId || ''; + + if (options.name) { + await expect(page.locator('input[id="name"]')).toHaveValue(options.name); + } + if (options.email) { + await expect(page.locator('input[id="email"]')).toHaveValue(options.email); + } + if (options.phone) { + await expect(page.locator('input[id="phone"]')).toHaveValue(options.phone); + } + + return { + id: userId, + name: options.name, + email: options.email, + phone: options.phone + }; + }); +} + +export async function searchUser(page: Page, query: string): Promise { + return test.step(`search user: ${query}`, async () => { + const searchInput = page.getByPlaceholder(/Search by name, email, phone, or ID/i); + await searchInput.clear(); + await searchInput.fill(query); + await searchInput.press('Enter'); + await page.waitForURL(/[?&]search=/); + await expect(searchInput).toHaveValue(query); + }); +} + +export async function deleteUser( + page: Page, + region: string, + projectId: string, + userId: string +): Promise { + return test.step(`delete user ${userId}`, async () => { + await navigateToUser(page, region, projectId, userId); + + await page.getByRole('heading', { name: 'Delete user' }).scrollIntoViewIfNeeded(); + await page.getByRole('button', { name: 'Delete', exact: true }).first().click(); + + const dialog = page.locator('dialog[open]'); + await dialog.waitFor({ state: 'visible' }); + await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + + await page.waitForURL(buildAuthUrlPattern(region, projectId, '$')); + await expect(page.getByText(/has been deleted/i)).toBeVisible(); + + await searchUser(page, userId); + const userRow = page.locator('[role="row"]').filter({ hasText: userId }); + await expect(userRow).not.toBeVisible(); + + await page.getByPlaceholder(/Search by name, email, phone, or ID/i).clear(); + }); +} + +export async function updateUserName( + page: Page, + region: string, + projectId: string, + userId: string, + newName: string +): Promise { + return test.step(`update user name to: ${newName}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const nameSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Name' }) + }); + await nameSection.locator('id=name').fill(newName); + await nameSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/has been updated/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('input[id="name"]')).toHaveValue(newName); + }); +} + +export async function updateUserEmail( + page: Page, + region: string, + projectId: string, + userId: string, + newEmail: string +): Promise { + return test.step(`update user email to: ${newEmail}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const emailSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Email' }) + }); + await emailSection.locator('id=email').fill(newEmail); + await emailSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/has been updated/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('input[id="email"]')).toHaveValue(newEmail); + }); +} + +export async function updateUserPhone( + page: Page, + region: string, + projectId: string, + userId: string, + newPhone: string +): Promise { + return test.step(`update user phone to: ${newPhone}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const phoneSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Phone' }) + }); + await phoneSection.locator('id=phone').fill(newPhone); + await phoneSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/has been updated/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('input[id="phone"]')).toHaveValue(newPhone); + }); +} + +export async function updateUserPassword( + page: Page, + region: string, + projectId: string, + userId: string, + newPassword: string +): Promise { + return test.step(`update user password`, async () => { + await navigateToUser(page, region, projectId, userId); + + const passwordSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Password' }) + }); + await passwordSection.locator('#newPassword').fill(newPassword); + await passwordSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/has been updated/i)).toBeVisible(); + }); +} + +export async function updateUserStatus( + page: Page, + region: string, + projectId: string, + userId: string, + enabled: boolean +): Promise { + return test.step(`update user status to: ${enabled ? 'unblocked' : 'blocked'}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const buttonText = enabled ? 'Unblock account' : 'Block account'; + const button = page.getByRole('button', { name: buttonText }); + await button.waitFor({ state: 'visible', timeout: 10000 }); + await button.click(); + await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + if (!enabled) { + await expect(page.getByText('blocked')).toBeVisible(); + } else { + await expect(page.getByText('blocked')).not.toBeVisible(); + } + }); +} + +export async function updateUserLabels( + page: Page, + region: string, + projectId: string, + userId: string, + labels: string[] +): Promise { + return test.step(`update user labels: ${labels.join(', ')}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const labelsSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Labels' }) + }); + + const tagsInput = labelsSection.locator('input[id="user-labels"]'); + await tagsInput.scrollIntoViewIfNeeded(); + + const existingTags = labelsSection.locator('[role="button"]').filter({ hasText: /×/i }); + const count = await existingTags.count(); + for (let i = 0; i < count; i++) { + await existingTags.first().click(); + } + + for (const label of labels) { + await tagsInput.fill(label); + await tagsInput.press('Enter'); + } + + const updateButton = labelsSection.getByRole('button', { name: 'Update' }); + await expect(updateButton).toBeEnabled({ timeout: 5000 }); + await updateButton.click(); + await expect(page.getByText(/have been updated/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + const reloadedLabelsSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Labels' }) + }); + for (const label of labels) { + await expect(reloadedLabelsSection.getByText(label)).toBeVisible(); + } + }); +} + +export async function updateUserEmailVerification( + page: Page, + region: string, + projectId: string, + userId: string, + shouldVerify: boolean +): Promise { + return test.step(`update user email verification to: ${shouldVerify}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); + await verifyButton.click(); + + await page.locator('ul.drop-list').waitFor({ state: 'visible' }); + + const dropdownItem = page + .locator('ul.drop-list li.drop-list-item') + .filter({ hasText: /(Verify|Unverify) email/ }) + .locator('button'); + await dropdownItem.click({ force: true }); + }); +} + +export async function updateUserPhoneVerification( + page: Page, + region: string, + projectId: string, + userId: string, + shouldVerify: boolean +): Promise { + return test.step(`update user phone verification to: ${shouldVerify}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); + await verifyButton.click(); + + await page.locator('ul.drop-list').waitFor({ state: 'visible' }); + + const dropdownItem = page + .locator('ul.drop-list li.drop-list-item') + .filter({ hasText: /(Verify|Unverify) phone/ }) + .locator('button'); + await dropdownItem.click({ force: true }); + }); +} + +export async function updateUserPrefs( + page: Page, + region: string, + projectId: string, + userId: string, + prefs: UserPrefs +): Promise { + return test.step(`update user preferences`, async () => { + await navigateToUser(page, region, projectId, userId); + + const prefsSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Preferences' }) + }); + + const deleteButtons = prefsSection.locator('button:has(.icon-x)'); + + const count = await deleteButtons.count(); + for (let i = 0; i < count; i++) { + await deleteButtons.first().click(); + } + + const prefEntries = Object.entries(prefs); + for (let i = 0; i < prefEntries.length; i++) { + const [key, value] = prefEntries[i]; + + const keyInput = prefsSection.locator(`id=key-${i}`); + const valueInput = prefsSection.locator(`id=value-${i}`); + + await keyInput.fill(key); + await valueInput.fill(value); + + if (i < prefEntries.length - 1) { + await prefsSection.getByRole('button', { name: 'Add preference' }).click(); + } + } + + await prefsSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/Preferences have been updated/i)).toBeVisible(); + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + }); +} + +export async function updateUserMfa( + page: Page, + region: string, + projectId: string, + userId: string, + enable: boolean +): Promise { + return test.step(`update user MFA to: ${enable ? 'enabled' : 'disabled'}`, async () => { + await navigateToUser(page, region, projectId, userId); + + const mfaSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Multi-factor authentication' }) + }); + + const mfaToggle = mfaSection.getByRole('switch'); + const isCurrentlyEnabled = (await mfaToggle.getAttribute('aria-checked')) === 'true'; + + if (isCurrentlyEnabled !== enable) { + await mfaToggle.click(); + await mfaSection.getByRole('button', { name: 'Update' }).click(); + await expect(page.getByText(/Multi-factor authentication has been/i)).toBeVisible(); + } + + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + const reloadedMfaSection = page.locator('form').filter({ + has: page.getByRole('heading', { name: 'Multi-factor authentication' }) + }); + const verifyToggle = reloadedMfaSection.getByRole('switch'); + if (enable) { + await expect(verifyToggle).toHaveAttribute('aria-checked', 'true'); + } else { + await expect(verifyToggle).toHaveAttribute('aria-checked', 'false'); + } + }); +} diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts new file mode 100644 index 0000000000..3c4b88bfff --- /dev/null +++ b/e2e/journeys/auth-free.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; +import { registerUserStep } from '../steps/account'; +import { createFreeProject } from '../steps/free-project'; +import { + createUser, + searchUser, + updateUserName, + updateUserEmail, + updateUserPhone, + updateUserPassword, + updateUserStatus, + updateUserLabels, + updateUserEmailVerification, + updateUserPhoneVerification, + updateUserPrefs, + updateUserMfa, + deleteUser +} from '../auth/users'; +import { navigateToUsers } from '../auth/navigation'; + +test('auth flow - free tier', async ({ page }) => { + await registerUserStep(page); + const project = await createFreeProject(page); + + const user = await createUser(page, 'nyc', project.id, { + name: 'Test User', + email: 'testuser@example.com', + phone: '+12345678901', + password: 'password123' + }); + + await test.step('verify user appears in list', async () => { + await navigateToUsers(page, 'nyc', project.id); + await expect(page.getByText('Test User')).toBeVisible(); + await expect(page.getByText('testuser@example.com')).toBeVisible(); + }); + + const user2 = await createUser(page, 'nyc', project.id, { + name: 'Second User', + email: 'second@second.com', + password: 'password456' + }); + + const user3 = await createUser(page, 'nyc', project.id, { + name: 'Third User', + email: 'third@example.com', + phone: '+13334445555', + password: 'password789' + }); + + await updateUserName(page, 'nyc', project.id, user.id, 'Updated Test User'); + await updateUserEmail(page, 'nyc', project.id, user.id, 'updated@example.com'); + await updateUserPhone(page, 'nyc', project.id, user.id, '+19876543210'); + await updateUserPassword(page, 'nyc', project.id, user.id, 'newpassword123'); + + await updateUserStatus(page, 'nyc', project.id, user.id, false); + await test.step('verify blocked status', async () => { + await navigateToUsers(page, 'nyc', project.id); + const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); + await expect(userRow.getByText('blocked')).toBeVisible(); + }); + + await updateUserStatus(page, 'nyc', project.id, user.id, true); + await test.step('verify unblocked status', async () => { + await navigateToUsers(page, 'nyc', project.id); + const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); + await expect(userRow.getByText('blocked')).not.toBeVisible(); + }); + + await updateUserLabels(page, 'nyc', project.id, user.id, ['test', 'e2e', 'freeTier']); + + await test.step('search by name', async () => { + await navigateToUsers(page, 'nyc', project.id); + await searchUser(page, 'Updated'); + await expect(page.getByText('Updated Test User')).toBeVisible(); + await expect(page.getByText('Second User')).not.toBeVisible(); + await expect(page.getByText('Third User')).not.toBeVisible(); + }); + + await test.step('search by email', async () => { + await navigateToUsers(page, 'nyc', project.id); + await searchUser(page, 'updated@example.com'); + await expect(page.getByText('updated@example.com')).toBeVisible(); + await expect(page.getByText('second@second.com')).not.toBeVisible(); + }); + + await test.step('verify multiple users', async () => { + await navigateToUsers(page, 'nyc', project.id); + await expect(page.getByText('Updated Test User')).toBeVisible(); + await expect(page.getByText('Second User')).toBeVisible(); + await expect(page.getByText('Third User')).toBeVisible(); + }); + + await test.step('test email and phone verification', async () => { + const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); + await expect(userRow.getByText('unverified')).toBeVisible(); + + await updateUserEmailVerification(page, 'nyc', project.id, user.id, true); + await navigateToUsers(page, 'nyc', project.id); + await expect(userRow.getByText('verified email')).toBeVisible(); + + await updateUserPhoneVerification(page, 'nyc', project.id, user.id, true); + await navigateToUsers(page, 'nyc', project.id); + await expect(userRow.getByText('verified')).toBeVisible(); + + await updateUserPhoneVerification(page, 'nyc', project.id, user.id, false); + await navigateToUsers(page, 'nyc', project.id); + await expect(userRow.getByText('verified email')).toBeVisible(); + }); + + await test.step('test user preferences', async () => { + await updateUserPrefs(page, 'nyc', project.id, user.id, { + theme: 'dark', + language: 'en', + timezone: 'UTC' + }); + }); + + await test.step('test MFA toggle', async () => { + await updateUserMfa(page, 'nyc', project.id, user.id, true); + await updateUserMfa(page, 'nyc', project.id, user.id, false); + }); + + await deleteUser(page, 'nyc', project.id, user.id); + await deleteUser(page, 'nyc', project.id, user2.id); + await deleteUser(page, 'nyc', project.id, user3.id); + + await test.step('verify users deleted', async () => { + await navigateToUsers(page, 'nyc', project.id); + await expect(page.getByText('Updated Test User')).not.toBeVisible(); + await expect(page.getByText('Second User')).not.toBeVisible(); + await expect(page.getByText('Third User')).not.toBeVisible(); + }); +}); From 04415385b9bda7ee9a7693535681b3a62de26c5e Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 30 Oct 2025 15:36:55 +0530 Subject: [PATCH 03/19] use: faster tests via dev mode. --- playwright.config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index fbd977757f..eacba322da 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ const config: PlaywrightTestConfig = { retries: 3, testDir: 'e2e', use: { - baseURL: 'http://localhost:4173/console/', + baseURL: 'http://localhost:3000/console/', trace: 'on-first-retry' }, webServer: { @@ -19,8 +19,9 @@ const config: PlaywrightTestConfig = { PUBLIC_STRIPE_KEY: 'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn' }, - command: 'pnpm run build && pnpm run preview', - port: 4173 + command: 'pnpm run dev', + port: 3000, + reuseExistingServer: true } }; From e92c287f5422a6fac113ab4a262ed4858b2b3eb5 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 31 Oct 2025 17:38:12 +0530 Subject: [PATCH 04/19] updates. --- e2e/auth/navigation.ts | 4 +- e2e/auth/users.ts | 20 +--- e2e/helpers/delete.ts | 97 +++++++++++++++++++ e2e/journeys/auth-free.spec.ts | 64 ++++++------ e2e/steps/free-project.ts | 11 ++- src/routes/(console)/account/delete.svelte | 2 +- .../settings/+page.svelte | 4 +- .../settings/deleteProject.svelte | 2 +- 8 files changed, 148 insertions(+), 56 deletions(-) create mode 100644 e2e/helpers/delete.ts diff --git a/e2e/auth/navigation.ts b/e2e/auth/navigation.ts index 21f4de519b..a6223a90b5 100644 --- a/e2e/auth/navigation.ts +++ b/e2e/auth/navigation.ts @@ -16,7 +16,7 @@ export async function navigateToUsers( return test.step('navigate to users', async () => { await page.goto(buildAuthUrl(region, projectId)); await page.waitForURL(buildAuthUrlPattern(region, projectId)); - await page.waitForLoadState('domcontentloaded'); + await page.getByRole('heading', { name: 'Auth' }).waitFor({ state: 'visible' }); }); } @@ -29,7 +29,7 @@ export async function navigateToUser( return test.step(`navigate to user ${userId}`, async () => { await page.goto(buildAuthUrl(region, projectId, `/user-${userId}`)); await page.waitForURL(buildAuthUrlPattern(region, projectId, `/user-${userId}`)); - await page.waitForLoadState('domcontentloaded'); + await page.getByText(userId).first().waitFor({ state: 'visible' }); }); } diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index cf755664a1..b5eaae2603 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -137,9 +137,6 @@ export async function updateUserName( await nameSection.locator('id=name').fill(newName); await nameSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/has been updated/i)).toBeVisible(); - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); await expect(page.locator('input[id="name"]')).toHaveValue(newName); }); } @@ -160,9 +157,6 @@ export async function updateUserEmail( await emailSection.locator('id=email').fill(newEmail); await emailSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/has been updated/i)).toBeVisible(); - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); await expect(page.locator('input[id="email"]')).toHaveValue(newEmail); }); } @@ -183,9 +177,6 @@ export async function updateUserPhone( await phoneSection.locator('id=phone').fill(newPhone); await phoneSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/has been updated/i)).toBeVisible(); - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); await expect(page.locator('input[id="phone"]')).toHaveValue(newPhone); }); } @@ -226,7 +217,7 @@ export async function updateUserStatus( await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); await page.reload(); - await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); if (!enabled) { await expect(page.getByText('blocked')).toBeVisible(); } else { @@ -267,9 +258,6 @@ export async function updateUserLabels( await expect(updateButton).toBeEnabled({ timeout: 5000 }); await updateButton.click(); await expect(page.getByText(/have been updated/i)).toBeVisible(); - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); const reloadedLabelsSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Labels' }) }); @@ -363,9 +351,6 @@ export async function updateUserPrefs( await prefsSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/Preferences have been updated/i)).toBeVisible(); - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); }); } @@ -391,9 +376,6 @@ export async function updateUserMfa( await mfaSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/Multi-factor authentication has been/i)).toBeVisible(); } - - await page.reload(); - await page.waitForLoadState('domcontentloaded'); const reloadedMfaSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Multi-factor authentication' }) }); diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts new file mode 100644 index 0000000000..b5c2eb0684 --- /dev/null +++ b/e2e/helpers/delete.ts @@ -0,0 +1,97 @@ +import { test, type Page, expect } from '@playwright/test'; + +export async function deleteProject(page: Page, region: string, projectId: string) { + return test.step('delete project', async () => { + await page.goto(`./project-${region}-${projectId}/settings`); + + // Get the project name from the data attribute + const projectName = await page.locator('[data-project-name]').textContent(); + + // Click the Delete button in the CardGrid actions section + await page.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for modal to open + const dialog = page.locator('dialog[open]'); + await expect(dialog).toBeVisible(); + + // Type the project name to confirm + await dialog.locator('#project-name').fill(projectName?.trim() || ''); + + // Click the Delete button in the modal + await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for navigation back to organization + await page.waitForURL(/\/organization-[^/]+$/); + }); +} + +export async function deleteOrganization(page: Page, organizationId: string) { + return test.step('delete organization', async () => { + await page.goto(`./organization-${organizationId}/settings`); + + // Get the organization name from the data attribute + const organizationName = await page.locator('[data-organization-name]').textContent(); + + // Click the Delete button in the CardGrid actions section + await page.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for modal to open + const dialog = page.locator('dialog[open]'); + await expect(dialog).toBeVisible(); + + // Type the organization name to confirm + await dialog.locator('#organization-name').fill(organizationName?.trim() || ''); + + // Click the Delete button in the modal + await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for navigation away from organization (to account/organizations or onboarding) + await page.waitForURL(/\/(account\/organizations|onboarding\/create-organization)/); + }); +} + +export async function deleteAccount(page: Page) { + return test.step('delete account', async () => { + await page.goto('./account'); + + // Click the Delete button in the CardGrid actions section + await page.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for confirm modal to open + const dialog = page.locator('dialog[open]'); + await expect(dialog).toBeVisible(); + + // Click the confirm button in the modal (no name typing required) + await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + + // Wait for navigation to login page after account deletion + await page.waitForURL(/login/); + }); +} + +export async function cleanupTestAccount( + page: Page, + region: string, + projectId: string, + organizationId: string +) { + return test.step('cleanup test account', async () => { + try { + await deleteProject(page, region, projectId); + } catch (error) { + console.log('Failed to delete project:', error); + } + + try { + await deleteOrganization(page, organizationId); + } catch (error) { + console.log('Failed to delete organization:', error); + } + + try { + await deleteAccount(page); + } catch (error) { + console.log('Failed to delete account:', error); + } + }); +} diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts index 3c4b88bfff..7bc5ca55ec 100644 --- a/e2e/journeys/auth-free.spec.ts +++ b/e2e/journeys/auth-free.spec.ts @@ -17,12 +17,13 @@ import { deleteUser } from '../auth/users'; import { navigateToUsers } from '../auth/navigation'; +import { cleanupTestAccount } from '../helpers/delete'; test('auth flow - free tier', async ({ page }) => { await registerUserStep(page); const project = await createFreeProject(page); - const user = await createUser(page, 'nyc', project.id, { + const user = await createUser(page, project.region, project.id, { name: 'Test User', email: 'testuser@example.com', phone: '+12345678901', @@ -30,47 +31,47 @@ test('auth flow - free tier', async ({ page }) => { }); await test.step('verify user appears in list', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); await expect(page.getByText('Test User')).toBeVisible(); await expect(page.getByText('testuser@example.com')).toBeVisible(); }); - const user2 = await createUser(page, 'nyc', project.id, { + const user2 = await createUser(page, project.region, project.id, { name: 'Second User', email: 'second@second.com', password: 'password456' }); - const user3 = await createUser(page, 'nyc', project.id, { + const user3 = await createUser(page, project.region, project.id, { name: 'Third User', email: 'third@example.com', phone: '+13334445555', password: 'password789' }); - await updateUserName(page, 'nyc', project.id, user.id, 'Updated Test User'); - await updateUserEmail(page, 'nyc', project.id, user.id, 'updated@example.com'); - await updateUserPhone(page, 'nyc', project.id, user.id, '+19876543210'); - await updateUserPassword(page, 'nyc', project.id, user.id, 'newpassword123'); + await updateUserName(page, project.region, project.id, user.id, 'Updated Test User'); + await updateUserEmail(page, project.region, project.id, user.id, 'updated@example.com'); + await updateUserPhone(page, project.region, project.id, user.id, '+19876543210'); + await updateUserPassword(page, project.region, project.id, user.id, 'newpassword123'); - await updateUserStatus(page, 'nyc', project.id, user.id, false); + await updateUserStatus(page, project.region, project.id, user.id, false); await test.step('verify blocked status', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); await expect(userRow.getByText('blocked')).toBeVisible(); }); - await updateUserStatus(page, 'nyc', project.id, user.id, true); + await updateUserStatus(page, project.region, project.id, user.id, true); await test.step('verify unblocked status', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); await expect(userRow.getByText('blocked')).not.toBeVisible(); }); - await updateUserLabels(page, 'nyc', project.id, user.id, ['test', 'e2e', 'freeTier']); + await updateUserLabels(page, project.region, project.id, user.id, ['test', 'e2e', 'freeTier']); await test.step('search by name', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); await searchUser(page, 'Updated'); await expect(page.getByText('Updated Test User')).toBeVisible(); await expect(page.getByText('Second User')).not.toBeVisible(); @@ -78,14 +79,14 @@ test('auth flow - free tier', async ({ page }) => { }); await test.step('search by email', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); await searchUser(page, 'updated@example.com'); await expect(page.getByText('updated@example.com')).toBeVisible(); await expect(page.getByText('second@second.com')).not.toBeVisible(); }); await test.step('verify multiple users', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); await expect(page.getByText('Updated Test User')).toBeVisible(); await expect(page.getByText('Second User')).toBeVisible(); await expect(page.getByText('Third User')).toBeVisible(); @@ -95,21 +96,21 @@ test('auth flow - free tier', async ({ page }) => { const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); await expect(userRow.getByText('unverified')).toBeVisible(); - await updateUserEmailVerification(page, 'nyc', project.id, user.id, true); - await navigateToUsers(page, 'nyc', project.id); + await updateUserEmailVerification(page, project.region, project.id, user.id, true); + await navigateToUsers(page, project.region, project.id); await expect(userRow.getByText('verified email')).toBeVisible(); - await updateUserPhoneVerification(page, 'nyc', project.id, user.id, true); - await navigateToUsers(page, 'nyc', project.id); + await updateUserPhoneVerification(page, project.region, project.id, user.id, true); + await navigateToUsers(page, project.region, project.id); await expect(userRow.getByText('verified')).toBeVisible(); - await updateUserPhoneVerification(page, 'nyc', project.id, user.id, false); - await navigateToUsers(page, 'nyc', project.id); + await updateUserPhoneVerification(page, project.region, project.id, user.id, false); + await navigateToUsers(page, project.region, project.id); await expect(userRow.getByText('verified email')).toBeVisible(); }); await test.step('test user preferences', async () => { - await updateUserPrefs(page, 'nyc', project.id, user.id, { + await updateUserPrefs(page, project.region, project.id, user.id, { theme: 'dark', language: 'en', timezone: 'UTC' @@ -117,18 +118,23 @@ test('auth flow - free tier', async ({ page }) => { }); await test.step('test MFA toggle', async () => { - await updateUserMfa(page, 'nyc', project.id, user.id, true); - await updateUserMfa(page, 'nyc', project.id, user.id, false); + await updateUserMfa(page, project.region, project.id, user.id, true); + await updateUserMfa(page, project.region, project.id, user.id, false); }); - await deleteUser(page, 'nyc', project.id, user.id); - await deleteUser(page, 'nyc', project.id, user2.id); - await deleteUser(page, 'nyc', project.id, user3.id); + await deleteUser(page, project.region, project.id, user.id); + await deleteUser(page, project.region, project.id, user2.id); + await deleteUser(page, project.region, project.id, user3.id); await test.step('verify users deleted', async () => { - await navigateToUsers(page, 'nyc', project.id); + await navigateToUsers(page, project.region, project.id); await expect(page.getByText('Updated Test User')).not.toBeVisible(); await expect(page.getByText('Second User')).not.toBeVisible(); await expect(page.getByText('Third User')).not.toBeVisible(); }); + + // cleanup: delete project, organization, and account + test.afterAll('tear down', async () => { + await cleanupTestAccount(page, project.region, project.id, project.organizationId); + }) }); diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index cf0972e478..e67241e8af 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -4,6 +4,7 @@ import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url'; type Metadata = { id: string; organizationId: string; + region: string; }; export async function createFreeProject(page: Page): Promise { @@ -13,7 +14,7 @@ export async function createFreeProject(page: Page): Promise { return getOrganizationIdFromUrl(page.url()); }); - const projectId = await test.step('create project', async () => { + const { projectId, region } = await test.step('create project', async () => { await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); const dialog = page.locator('dialog[open]'); @@ -34,11 +35,15 @@ export async function createFreeProject(page: Page): Promise { await page.waitForURL(new RegExp(`/project-${region}-[^/]+`)); expect(page.url()).toContain(`/console/project-${region}-`); - return getProjectIdFromUrl(page.url()); + return { + projectId: getProjectIdFromUrl(page.url()), + region + }; }); return { id: projectId, - organizationId + organizationId, + region }; } diff --git a/src/routes/(console)/account/delete.svelte b/src/routes/(console)/account/delete.svelte index b7bf6fa151..e391ea1463 100644 --- a/src/routes/(console)/account/delete.svelte +++ b/src/routes/(console)/account/delete.svelte @@ -16,7 +16,7 @@ showDelete = false; addNotification({ type: 'success', - message: `Account was deleted ` + message: 'Account was deleted' }); trackEvent(Submit.AccountDelete); } catch (e) { diff --git a/src/routes/(console)/organization-[organization]/settings/+page.svelte b/src/routes/(console)/organization-[organization]/settings/+page.svelte index b5bf05fc0d..ca84716806 100644 --- a/src/routes/(console)/organization-[organization]/settings/+page.svelte +++ b/src/routes/(console)/organization-[organization]/settings/+page.svelte @@ -89,7 +89,9 @@ -
{$organization.name}
+
+ {$organization.name} +

{orgMembers}, {orgProjects}

diff --git a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte index 0af70f0083..2464f33961 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte @@ -49,7 +49,7 @@ -
{$project.name}
+
{$project.name}
{#if isCloud && $projectRegion}

Region: {$projectRegion.name}

From 43f08ae5b78dd4551e8ba72c089a04a7aaf7dcf7 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 31 Oct 2025 20:09:15 +0530 Subject: [PATCH 05/19] updates. --- e2e/auth/navigation.ts | 12 +++++ e2e/auth/users.ts | 24 ++++----- e2e/fixtures/base.ts | 44 ++++++++++++++++ e2e/helpers/delete.ts | 52 ++++++++++++++----- e2e/journeys/auth-free.spec.ts | 19 ++----- e2e/journeys/onboarding-free.spec.ts | 11 ++-- e2e/journeys/onboarding-pro.spec.ts | 13 ++--- e2e/journeys/upgrade-free-tier.spec.ts | 6 +-- e2e/steps/free-project.ts | 40 +++++++++----- e2e/steps/pro-project.ts | 45 ++++++++++------ .../auth/user-[user]/updateStatus.svelte | 2 +- 11 files changed, 177 insertions(+), 91 deletions(-) create mode 100644 e2e/fixtures/base.ts diff --git a/e2e/auth/navigation.ts b/e2e/auth/navigation.ts index a6223a90b5..3379fe0108 100644 --- a/e2e/auth/navigation.ts +++ b/e2e/auth/navigation.ts @@ -14,6 +14,11 @@ export async function navigateToUsers( projectId: string ): Promise { return test.step('navigate to users', async () => { + const expectedPattern = new RegExp(`/project-${region}-${projectId}/auth(/\\?.*)?$`); + if (expectedPattern.test(page.url())) { + return; + } + await page.goto(buildAuthUrl(region, projectId)); await page.waitForURL(buildAuthUrlPattern(region, projectId)); await page.getByRole('heading', { name: 'Auth' }).waitFor({ state: 'visible' }); @@ -27,6 +32,13 @@ export async function navigateToUser( userId: string ): Promise { return test.step(`navigate to user ${userId}`, async () => { + const expectedPattern = new RegExp( + `/project-${region}-${projectId}/auth/user-${userId}(/\\?.*)?$` + ); + if (expectedPattern.test(page.url())) { + return; + } + await page.goto(buildAuthUrl(region, projectId, `/user-${userId}`)); await page.waitForURL(buildAuthUrlPattern(region, projectId, `/user-${userId}`)); await page.getByText(userId).first().waitFor({ state: 'visible' }); diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index b5eaae2603..c4501bf92d 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -136,7 +136,7 @@ export async function updateUserName( }); await nameSection.locator('id=name').fill(newName); await nameSection.getByRole('button', { name: 'Update' }).click(); - await expect(page.getByText(/has been updated/i)).toBeVisible(); + await expect(page.getByText(/name has been updated/i)).toBeVisible(); await expect(page.locator('input[id="name"]')).toHaveValue(newName); }); } @@ -156,7 +156,7 @@ export async function updateUserEmail( }); await emailSection.locator('id=email').fill(newEmail); await emailSection.getByRole('button', { name: 'Update' }).click(); - await expect(page.getByText(/has been updated/i)).toBeVisible(); + await expect(page.getByText(/email has been updated/i)).toBeVisible(); await expect(page.locator('input[id="email"]')).toHaveValue(newEmail); }); } @@ -176,7 +176,7 @@ export async function updateUserPhone( }); await phoneSection.locator('id=phone').fill(newPhone); await phoneSection.getByRole('button', { name: 'Update' }).click(); - await expect(page.getByText(/has been updated/i)).toBeVisible(); + await expect(page.getByText(/phone has been updated/i)).toBeVisible(); await expect(page.locator('input[id="phone"]')).toHaveValue(newPhone); }); } @@ -196,7 +196,7 @@ export async function updateUserPassword( }); await passwordSection.locator('#newPassword').fill(newPassword); await passwordSection.getByRole('button', { name: 'Update' }).click(); - await expect(page.getByText(/has been updated/i)).toBeVisible(); + await expect(page.getByText(/password has been updated/i)).toBeVisible(); }); } @@ -216,12 +216,12 @@ export async function updateUserStatus( await button.click(); await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); - await page.reload(); - await page.waitForLoadState('networkidle'); + // Now verify the badge appears/disappears in the status section + const statusSection = page.locator('[data-user-status]'); if (!enabled) { - await expect(page.getByText('blocked')).toBeVisible(); + await expect(statusSection.getByText('blocked')).toBeVisible({ timeout: 5000 }); } else { - await expect(page.getByText('blocked')).not.toBeVisible(); + await expect(statusSection.getByText('blocked')).not.toBeVisible({ timeout: 5000 }); } }); } @@ -327,13 +327,9 @@ export async function updateUserPrefs( has: page.getByRole('heading', { name: 'Preferences' }) }); - const deleteButtons = prefsSection.locator('button:has(.icon-x)'); - - const count = await deleteButtons.count(); - for (let i = 0; i < count; i++) { - await deleteButtons.first().click(); - } + await prefsSection.scrollIntoViewIfNeeded(); + // fill preferences const prefEntries = Object.entries(prefs); for (let i = 0; i < prefEntries.length; i++) { const [key, value] = prefEntries[i]; diff --git a/e2e/fixtures/base.ts b/e2e/fixtures/base.ts new file mode 100644 index 0000000000..24dafa912c --- /dev/null +++ b/e2e/fixtures/base.ts @@ -0,0 +1,44 @@ +import { test as base } from '@playwright/test'; +import { cleanupTestAccount } from '../helpers/delete'; + +import { registerUserStep } from '../steps/account'; +import { createFreeProject } from '../steps/free-project'; +import { createProProject } from '../steps/pro-project'; + +export type ProjectMetadata = { + id: string; + region: string; + organizationId: string; +}; + +type ProjectFixtures = { + project: ProjectMetadata; + tier: 'free' | 'pro' | 'scale' /* for later */; +}; + +export const test = base.extend({ + tier: ['free', { option: true }], + project: [ + async ({ page, tier }, use) => { + await registerUserStep(page); + + let project: ProjectMetadata; + switch (tier) { + case 'free': + project = await createFreeProject(page); + break; + case 'pro': + case 'scale': + project = await createProProject(page); + break; + } + + await use(project); + + await cleanupTestAccount(page, project.region, project.id, project.organizationId); + }, + { auto: true } + ] +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index b5c2eb0684..0042cd7547 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -50,22 +50,46 @@ export async function deleteOrganization(page: Page, organizationId: string) { }); } -export async function deleteAccount(page: Page) { +export async function deleteAccount(page: Page, maxRetries = 3) { return test.step('delete account', async () => { - await page.goto('./account'); - - // Click the Delete button in the CardGrid actions section - await page.getByRole('button', { name: 'Delete', exact: true }).click(); - - // Wait for confirm modal to open - const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible(); - - // Click the confirm button in the modal (no name typing required) - await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + for (let attempt = 0; attempt < maxRetries; attempt++) { + await page.goto('./account'); + + // click the Delete button in the CardGrid actions section + await page.getByRole('button', { name: 'Delete', exact: true }).click(); + + // wait for confirm modal to open + const dialog = page.locator('dialog[open]'); + await expect(dialog).toBeVisible(); + + // click the confirm button in the modal (no name typing required) + await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + + // check if we got an error about active memberships + const membershipError = page.getByText(/active memberships/i); + const errorVisible = await membershipError + .isVisible({ timeout: 2000 }) + .catch(() => false); + + if (errorVisible) { + console.log( + `Attempt ${attempt + 1}: Account deletion failed due to active memberships. Retrying...` + ); + // close the dialog if still open + await page.keyboard.press('Escape').catch(() => {}); + // wait before retrying (org deletion might still be processing) + await page.waitForTimeout(2000); + continue; + } + + // wait for navigation to login page after account deletion + await page.waitForURL(/login/, { timeout: 5000 }); + return; + } - // Wait for navigation to login page after account deletion - await page.waitForURL(/login/); + throw new Error( + 'Failed to delete account after multiple retries due to active memberships' + ); }); } diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts index 7bc5ca55ec..a376fb7ab3 100644 --- a/e2e/journeys/auth-free.spec.ts +++ b/e2e/journeys/auth-free.spec.ts @@ -1,6 +1,4 @@ -import { test, expect } from '@playwright/test'; -import { registerUserStep } from '../steps/account'; -import { createFreeProject } from '../steps/free-project'; +import { test, expect } from '../fixtures/base'; import { createUser, searchUser, @@ -17,12 +15,8 @@ import { deleteUser } from '../auth/users'; import { navigateToUsers } from '../auth/navigation'; -import { cleanupTestAccount } from '../helpers/delete'; - -test('auth flow - free tier', async ({ page }) => { - await registerUserStep(page); - const project = await createFreeProject(page); +test('auth flow - free tier', async ({ page, project }) => { const user = await createUser(page, project.region, project.id, { name: 'Test User', email: 'testuser@example.com', @@ -58,14 +52,14 @@ test('auth flow - free tier', async ({ page }) => { await test.step('verify blocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).toBeVisible(); + await expect(userRow.getByText('blocked')).toBeVisible({ timeout: 10000 }); }); await updateUserStatus(page, project.region, project.id, user.id, true); await test.step('verify unblocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).not.toBeVisible(); + await expect(userRow.getByText('blocked')).not.toBeVisible({ timeout: 10000 }); }); await updateUserLabels(page, project.region, project.id, user.id, ['test', 'e2e', 'freeTier']); @@ -132,9 +126,4 @@ test('auth flow - free tier', async ({ page }) => { await expect(page.getByText('Second User')).not.toBeVisible(); await expect(page.getByText('Third User')).not.toBeVisible(); }); - - // cleanup: delete project, organization, and account - test.afterAll('tear down', async () => { - await cleanupTestAccount(page, project.region, project.id, project.organizationId); - }) }); diff --git a/e2e/journeys/onboarding-free.spec.ts b/e2e/journeys/onboarding-free.spec.ts index 622088d970..efa974fc9e 100644 --- a/e2e/journeys/onboarding-free.spec.ts +++ b/e2e/journeys/onboarding-free.spec.ts @@ -1,8 +1,7 @@ -import { test } from '@playwright/test'; -import { registerUserStep } from '../steps/account'; -import { createFreeProject } from '../steps/free-project'; +import { test, expect } from '../fixtures/base'; -test('onboarding - free tier', async ({ page }) => { - await registerUserStep(page); - await createFreeProject(page); +test('onboarding - free tier', async ({ page, project }) => { + await expect(page).toHaveURL( + new RegExp(`/project-${project.region}-${project.id}/get-started`) + ); }); diff --git a/e2e/journeys/onboarding-pro.spec.ts b/e2e/journeys/onboarding-pro.spec.ts index 53c22ef051..d808308539 100644 --- a/e2e/journeys/onboarding-pro.spec.ts +++ b/e2e/journeys/onboarding-pro.spec.ts @@ -1,8 +1,9 @@ -import { test } from '@playwright/test'; -import { registerUserStep } from '../steps/account'; -import { createProProject } from '../steps/pro-project'; +import { test, expect } from '../fixtures/base'; -test('onboarding - pro', async ({ page }) => { - await registerUserStep(page); - await createProProject(page); +test.use({ tier: 'pro' }); + +test('onboarding - pro', async ({ page, project }) => { + await expect(page).toHaveURL( + new RegExp(`/project-${project.region}-${project.id}/get-started`) + ); }); diff --git a/e2e/journeys/upgrade-free-tier.spec.ts b/e2e/journeys/upgrade-free-tier.spec.ts index 55c759a661..ce213a2b9f 100644 --- a/e2e/journeys/upgrade-free-tier.spec.ts +++ b/e2e/journeys/upgrade-free-tier.spec.ts @@ -1,11 +1,7 @@ -import { test } from '@playwright/test'; -import { registerUserStep } from '../steps/account'; -import { createFreeProject } from '../steps/free-project'; +import { test } from '../fixtures/base'; import { enterCreditCard } from '../steps/pro-project'; test('upgrade - free tier', async ({ page }) => { - await registerUserStep(page); - await createFreeProject(page); await test.step('upgrade project', async () => { await page.getByRole('link', { name: 'Upgrade', exact: true }).click(); await page.waitForURL(/\/organization-[^/]+\/change-plan/); diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index e67241e8af..41fc22235f 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -1,13 +1,8 @@ +import type { ProjectMetadata } from '../fixtures/base'; import { test, expect, type Page } from '@playwright/test'; import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url'; -type Metadata = { - id: string; - organizationId: string; - region: string; -}; - -export async function createFreeProject(page: Page): Promise { +export async function createFreeProject(page: Page): Promise { const organizationId = await test.step('create organization', async () => { await page.goto('./'); await page.waitForURL(/\/organization-[^/]+/); @@ -21,18 +16,35 @@ export async function createFreeProject(page: Page): Promise { await dialog.getByPlaceholder('Project name').fill('test project'); - let region = 'fra'; // for fallback - const regionPicker = dialog.locator('button[role="combobox"]'); - if (await regionPicker.isVisible()) { - await regionPicker.click(); - await page.getByRole('option', { name: /New York/i }).click(); + let region = 'fra'; + + // @ts-expect-error - process.env is available in Node.js test environment + const isMultiRegion = process.env.PUBLIC_APPWRITE_MULTI_REGION === 'true'; + + if (isMultiRegion) { + const regionPicker = dialog.locator('button[role="combobox"]'); + if (await regionPicker.isVisible()) { + await regionPicker.click(); - region = 'nyc'; + // get all available/enabled region options + const options = await page.getByRole('option').all(); + if (options.length > 0) { + // select a random region + const randomIndex = Math.floor(Math.random() * options.length); + await options[randomIndex].click(); + } + } } await dialog.getByRole('button', { name: 'create' }).click(); - await page.waitForURL(new RegExp(`/project-${region}-[^/]+`)); + // wait for URL and extract actual region from it + await page.waitForURL(/\/project-[^/]+-[^/]+/); + const urlMatch = page.url().match(/\/project-([^-]+)-/); + if (urlMatch) { + region = urlMatch[1]; + } + expect(page.url()).toContain(`/console/project-${region}-`); return { diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index 3f3a7ec1fe..de05b7cf60 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -1,11 +1,7 @@ +import type { ProjectMetadata } from '../fixtures/base'; import { test, expect, type Page } from '@playwright/test'; import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url'; -type Metadata = { - id: string; - organizationId: string; -}; - export async function enterCreditCard(page: Page) { // click the `add` button inside correct view layer await page @@ -31,7 +27,7 @@ export async function enterCreditCard(page: Page) { }); } -export async function createProProject(page: Page): Promise { +export async function createProProject(page: Page): Promise { const organizationId = await test.step('create organization', async () => { await page.goto('./create-organization'); await page.locator('id=name').fill('test org'); @@ -46,31 +42,48 @@ export async function createProProject(page: Page): Promise { return getOrganizationIdFromUrl(page.url()); }); - const projectId = await test.step('create project', async () => { + const { projectId, region } = await test.step('create project', async () => { await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); const dialog = page.locator('dialog[open]'); await dialog.getByPlaceholder('Project name').fill('test project'); - let region = 'fra'; // for fallback - const regionPicker = dialog.locator('button[role="combobox"]'); - if (await regionPicker.isVisible()) { - await regionPicker.click(); - await page.getByRole('option', { name: /New York/i }).click(); + let region = 'fra'; + // @ts-expect-error - process.env is available in Node.js test environment + const isMultiRegion = process.env.PUBLIC_APPWRITE_MULTI_REGION === 'true'; + + if (isMultiRegion) { + const regionPicker = dialog.locator('button[role="combobox"]'); + if (await regionPicker.isVisible()) { + await regionPicker.click(); - region = 'nyc'; + // get all available/enabled region options + const options = await page.getByRole('option').all(); + if (options.length > 0) { + // select a random region + const randomIndex = Math.floor(Math.random() * options.length); + await options[randomIndex].click(); + } + } } await dialog.getByRole('button', { name: 'create' }).click(); - await page.waitForURL(new RegExp(`/project-${region}-[^/]+`)); + + // wait for URL and extract actual region from it + await page.waitForURL(/\/project-[^/]+-[^/]+/); + const urlMatch = page.url().match(/\/project-([^-]+)-/); + if (urlMatch) { + region = urlMatch[1]; + } expect(page.url()).toContain(`/console/project-${region}-`); - return getProjectIdFromUrl(page.url()); + return { projectId: getProjectIdFromUrl(page.url()), region }; }); return { id: projectId, - organizationId + organizationId, + region }; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte index 88e389ee89..08da73ee25 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updateStatus.svelte @@ -122,7 +122,7 @@

Joined: {toLocaleDateTime($user.registration)}

Last activity: {accessedAt ? toLocaleDate(accessedAt) : 'never'}

-
+
{#if !$user.status} {:else if $user.email && $user.phone} From 051f6f649ac82573a099228ffc79b9cbd3ac4861 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 12:38:01 +0530 Subject: [PATCH 06/19] fix. --- e2e/helpers/region.ts | 33 +++++++++++ e2e/steps/free-project.ts | 27 +-------- e2e/steps/pro-project.ts | 25 +-------- package.json | 4 +- playwright.config.ts | 11 +++- pnpm-lock.yaml | 112 +++++++++++++++++++++----------------- 6 files changed, 110 insertions(+), 102 deletions(-) create mode 100644 e2e/helpers/region.ts diff --git a/e2e/helpers/region.ts b/e2e/helpers/region.ts new file mode 100644 index 0000000000..0c5a6775f6 --- /dev/null +++ b/e2e/helpers/region.ts @@ -0,0 +1,33 @@ +import type { Page, Locator } from '@playwright/test'; + +export async function selectRandomRegion(page: Page, dialog: Locator): Promise { + if (process.env.PUBLIC_APPWRITE_MULTI_REGION !== 'true') return 'fra'; + + const regionPicker = dialog.locator('button[role="combobox"]'); + if (await regionPicker.isVisible()) { + await regionPicker.click(); + + const options = await page + .getByRole('option') + .filter({ hasNot: page.locator('[aria-disabled="true"]') }) + .all(); + + if (options.length > 0) { + const randomIndex = Math.floor(Math.random() * options.length); + const selectedOption = options[randomIndex]; + + const regionCode = + (await selectedOption.getAttribute('data-value')) || + (await selectedOption.getAttribute('value')); + + await selectedOption.click(); + + // remove quotes if present in the attribute value + const cleanedRegionCode = regionCode?.replace(/^["']|["']$/g, ''); + + return cleanedRegionCode || 'fra'; + } + } + + return 'fra'; +} diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index 41fc22235f..52c9405f98 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -1,4 +1,5 @@ import type { ProjectMetadata } from '../fixtures/base'; +import { selectRandomRegion } from '../helpers/region'; import { test, expect, type Page } from '@playwright/test'; import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url'; @@ -16,35 +17,11 @@ export async function createFreeProject(page: Page): Promise { await dialog.getByPlaceholder('Project name').fill('test project'); - let region = 'fra'; - - // @ts-expect-error - process.env is available in Node.js test environment - const isMultiRegion = process.env.PUBLIC_APPWRITE_MULTI_REGION === 'true'; - - if (isMultiRegion) { - const regionPicker = dialog.locator('button[role="combobox"]'); - if (await regionPicker.isVisible()) { - await regionPicker.click(); - - // get all available/enabled region options - const options = await page.getByRole('option').all(); - if (options.length > 0) { - // select a random region - const randomIndex = Math.floor(Math.random() * options.length); - await options[randomIndex].click(); - } - } - } + const region = await selectRandomRegion(page, dialog); await dialog.getByRole('button', { name: 'create' }).click(); - // wait for URL and extract actual region from it await page.waitForURL(/\/project-[^/]+-[^/]+/); - const urlMatch = page.url().match(/\/project-([^-]+)-/); - if (urlMatch) { - region = urlMatch[1]; - } - expect(page.url()).toContain(`/console/project-${region}-`); return { diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index de05b7cf60..a99a64dd0e 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -1,6 +1,7 @@ import type { ProjectMetadata } from '../fixtures/base'; import { test, expect, type Page } from '@playwright/test'; import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url'; +import { selectRandomRegion } from '../helpers/region'; export async function enterCreditCard(page: Page) { // click the `add` button inside correct view layer @@ -49,33 +50,11 @@ export async function createProProject(page: Page): Promise { await dialog.getByPlaceholder('Project name').fill('test project'); - let region = 'fra'; - // @ts-expect-error - process.env is available in Node.js test environment - const isMultiRegion = process.env.PUBLIC_APPWRITE_MULTI_REGION === 'true'; - - if (isMultiRegion) { - const regionPicker = dialog.locator('button[role="combobox"]'); - if (await regionPicker.isVisible()) { - await regionPicker.click(); - - // get all available/enabled region options - const options = await page.getByRole('option').all(); - if (options.length > 0) { - // select a random region - const randomIndex = Math.floor(Math.random() * options.length); - await options[randomIndex].click(); - } - } - } + const region = await selectRandomRegion(page, dialog); await dialog.getByRole('button', { name: 'create' }).click(); - // wait for URL and extract actual region from it await page.waitForURL(/\/project-[^/]+-[^/]+/); - const urlMatch = page.url().match(/\/project-([^-]+)-/); - if (urlMatch) { - region = urlMatch[1]; - } expect(page.url()).toContain(`/console/project-${region}-`); return { projectId: getProjectIdFromUrl(page.url()), region }; diff --git a/package.json b/package.json index 5a2e77773d..8d45856dc6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "tippy.js": "^6.3.7" }, "devDependencies": { + "dotenv": "^17.2.3", "@eslint/compat": "^1.3.1", "@eslint/js": "^9.31.0", "@melt-ui/pp": "^0.3.2", @@ -85,7 +86,8 @@ "typescript": "^5.8.2", "typescript-eslint": "^8.30.1", "vite": "^7.0.6", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "@types/node": "^24.9.2" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/playwright.config.ts b/playwright.config.ts index eacba322da..fe6952ed6a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,8 @@ +import { config as loadEnv } from 'dotenv'; import { type PlaywrightTestConfig } from '@playwright/test'; +loadEnv(); + const config: PlaywrightTestConfig = { timeout: 120000, reportSlowTests: null, @@ -13,10 +16,12 @@ const config: PlaywrightTestConfig = { webServer: { timeout: 120000, env: { - PUBLIC_APPWRITE_ENDPOINT: 'https://stage.cloud.appwrite.io/v1', - PUBLIC_CONSOLE_MODE: 'cloud', - PUBLIC_APPWRITE_MULTI_REGION: 'true', + PUBLIC_APPWRITE_ENDPOINT: + process.env.PUBLIC_APPWRITE_ENDPOINT || 'https://stage.cloud.appwrite.io/v1', + PUBLIC_CONSOLE_MODE: process.env.PUBLIC_CONSOLE_MODE || 'cloud', + PUBLIC_APPWRITE_MULTI_REGION: process.env.PUBLIC_APPWRITE_MULTI_REGION || 'true', PUBLIC_STRIPE_KEY: + process.env.PUBLIC_STRIPE_KEY || 'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn' }, command: 'pnpm run dev', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82b9a6afc8..c470f9a29c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 2.11.8 '@sentry/sveltekit': specifier: ^8.38.0 - version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) '@stripe/stripe-js': specifier: ^3.5.0 version: 3.5.0 @@ -101,13 +101,13 @@ importers: version: 1.56.1 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))) + version: 3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))) '@sveltejs/kit': specifier: ^2.42.1 - version: 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + version: 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.3 - version: 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + version: 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -116,13 +116,16 @@ importers: version: 6.6.3 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.8(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))(vitest@3.2.4) + version: 5.2.8(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))(vitest@3.2.4) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) '@types/deep-equal': specifier: ^1.0.4 version: 1.0.4 + '@types/node': + specifier: ^24.9.2 + version: 24.9.2 '@types/prismjs': specifier: ^1.26.5 version: 1.26.5 @@ -141,6 +144,9 @@ importers: color: specifier: ^5.0.0 version: 5.0.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 eslint: specifier: ^9.31.0 version: 9.31.0 @@ -194,10 +200,10 @@ importers: version: 8.30.1(eslint@9.31.0)(typescript@5.8.2) vite: specifier: ^7.0.6 - version: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + version: 7.0.6(@types/node@24.9.2)(sass@1.86.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) packages: @@ -1369,8 +1375,8 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@22.13.14': - resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} + '@types/node@24.9.2': + resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -2035,6 +2041,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3387,8 +3397,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -4674,19 +4684,19 @@ snapshots: magic-string: 0.30.7 svelte: 5.25.3 - '@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))': + '@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))': dependencies: '@sentry/core': 8.55.0 '@sentry/node': 8.55.0 '@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0) '@sentry/svelte': 8.55.0(svelte@5.25.3) '@sentry/vite-plugin': 2.22.6 - '@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) magic-string: 0.30.7 magicast: 0.2.8 sorcery: 1.0.0 optionalDependencies: - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/context-async-hooks' @@ -4753,15 +4763,15 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))': dependencies: - '@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) - '@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))': + '@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -4774,29 +4784,29 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.1 svelte: 5.25.3 - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) optionalDependencies: '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) debug: 4.4.0 svelte: 5.25.3 - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))': + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 5.25.3 - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) - vitefu: 1.0.6(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) + vitefu: 1.0.6(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) transitivePeerDependencies: - supports-color @@ -4832,13 +4842,13 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/svelte@5.2.8(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))(vitest@3.2.4)': + '@testing-library/svelte@5.2.8(svelte@5.25.3)(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.25.3 optionalDependencies: - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) - vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: @@ -4852,7 +4862,7 @@ snapshots: '@types/connect@3.4.36': dependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 '@types/cookie@0.6.0': {} @@ -4878,11 +4888,11 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 - '@types/node@22.13.14': + '@types/node@24.9.2': dependencies: - undici-types: 6.20.0 + undici-types: 7.16.0 '@types/pg-pool@2.0.6': dependencies: @@ -4890,7 +4900,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 pg-protocol: 1.8.0 pg-types: 2.2.0 @@ -4909,7 +4919,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 '@types/unist@3.0.3': {} @@ -5077,13 +5087,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))': + '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -5114,7 +5124,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0) '@vitest/utils@3.2.4': dependencies: @@ -5672,6 +5682,8 @@ snapshots: dotenv@16.4.7: {} + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7048,7 +7060,7 @@ snapshots: typescript@5.8.2: {} - undici-types@6.20.0: {} + undici-types@7.16.0: {} unist-util-is@6.0.0: dependencies: @@ -7106,13 +7118,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.13.14)(sass@1.86.0): + vite-node@3.2.4(@types/node@24.9.2)(sass@1.86.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7127,7 +7139,7 @@ snapshots: - tsx - yaml - vite@7.0.6(@types/node@22.13.14)(sass@1.86.0): + vite@7.0.6(@types/node@24.9.2)(sass@1.86.0): dependencies: esbuild: 0.25.1 fdir: 6.4.6(picomatch@4.0.3) @@ -7136,19 +7148,19 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 fsevents: 2.3.3 sass: 1.86.0 - vitefu@1.0.6(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)): + vitefu@1.0.6(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)): optionalDependencies: - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) - vitest@3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)) + '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@24.9.2)(sass@1.86.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7166,11 +7178,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.6(@types/node@22.13.14)(sass@1.86.0) - vite-node: 3.2.4(@types/node@22.13.14)(sass@1.86.0) + vite: 7.0.6(@types/node@24.9.2)(sass@1.86.0) + vite-node: 3.2.4(@types/node@24.9.2)(sass@1.86.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.13.14 + '@types/node': 24.9.2 '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: From 626530f826b897e38bdb5cf7b36c07e86b7aadf5 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 12:43:12 +0530 Subject: [PATCH 07/19] ignore: last redirect. --- e2e/helpers/delete.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index 0042cd7547..58cee205d2 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -81,9 +81,6 @@ export async function deleteAccount(page: Page, maxRetries = 3) { await page.waitForTimeout(2000); continue; } - - // wait for navigation to login page after account deletion - await page.waitForURL(/login/, { timeout: 5000 }); return; } From 73694425e00ff4f3087b037eb03c5c4bf3bd2f61 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 13:03:15 +0530 Subject: [PATCH 08/19] update: ci and tests. --- .github/workflows/e2e.yml | 18 ++++++++++++++++-- .github/workflows/tests.yml | 11 ++++++++++- e2e/auth/navigation.ts | 3 ++- e2e/auth/users.ts | 29 ++++++++++++++++++++++++++++- package.json | 2 +- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 862b7ef450..d974469ee5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,16 +12,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 + cache: 'pnpm' + - name: Install pnpm uses: pnpm/action-setup@v4 + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + playwright-${{ runner.os }}- + - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps chromium + run: pnpm exec playwright install chromium + - name: E2E Tests run: pnpm run e2e - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6bd4d86ce9..a7f699dd18 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,21 +15,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 + cache: 'pnpm' + - name: Install pnpm uses: pnpm/action-setup@v4 + - name: Audit dependencies run: pnpm audit --audit-level high + - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Svelte Diagnostics run: pnpm run check + - name: Linter run: pnpm run lint + - name: Unit Tests run: pnpm run test + - name: Build Console run: pnpm run build diff --git a/e2e/auth/navigation.ts b/e2e/auth/navigation.ts index 3379fe0108..033e3e5893 100644 --- a/e2e/auth/navigation.ts +++ b/e2e/auth/navigation.ts @@ -35,13 +35,14 @@ export async function navigateToUser( const expectedPattern = new RegExp( `/project-${region}-${projectId}/auth/user-${userId}(/\\?.*)?$` ); + if (expectedPattern.test(page.url())) { return; } await page.goto(buildAuthUrl(region, projectId, `/user-${userId}`)); await page.waitForURL(buildAuthUrlPattern(region, projectId, `/user-${userId}`)); - await page.getByText(userId).first().waitFor({ state: 'visible' }); + await page.locator('input[id="name"]').waitFor({ state: 'attached' }); }); } diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index c4501bf92d..e133c2a5f7 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -20,6 +20,12 @@ export type UserPrefs = { [key: string]: string; }; +async function dismissNotification(page: Page, messagePattern: RegExp): Promise { + const notification = page.locator('.toast').filter({ hasText: messagePattern }); + await notification.getByRole('button').last().click(); + await expect(notification).not.toBeVisible(); +} + export async function createUser( page: Page, region: string, @@ -58,6 +64,7 @@ export async function createUser( await modal.getByRole('button', { name: 'Create', exact: true }).click(); await modal.waitFor({ state: 'hidden' }); await expect(page.getByText(/has been created/i)).toBeVisible(); + await dismissNotification(page, /has been created/i); await page.waitForURL(/\/auth\/user-[^/]+$/); const currentUrl = page.url(); @@ -112,7 +119,7 @@ export async function deleteUser( await page.waitForURL(buildAuthUrlPattern(region, projectId, '$')); await expect(page.getByText(/has been deleted/i)).toBeVisible(); - + await dismissNotification(page, /has been deleted/i); await searchUser(page, userId); const userRow = page.locator('[role="row"]').filter({ hasText: userId }); await expect(userRow).not.toBeVisible(); @@ -134,9 +141,11 @@ export async function updateUserName( const nameSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Name' }) }); + await nameSection.locator('id=name').fill(newName); await nameSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/name has been updated/i)).toBeVisible(); + await dismissNotification(page, /name has been updated/i); await expect(page.locator('input[id="name"]')).toHaveValue(newName); }); } @@ -154,9 +163,11 @@ export async function updateUserEmail( const emailSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Email' }) }); + await emailSection.locator('id=email').fill(newEmail); await emailSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/email has been updated/i)).toBeVisible(); + await dismissNotification(page, /email has been updated/i); await expect(page.locator('input[id="email"]')).toHaveValue(newEmail); }); } @@ -174,9 +185,11 @@ export async function updateUserPhone( const phoneSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Phone' }) }); + await phoneSection.locator('id=phone').fill(newPhone); await phoneSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/phone has been updated/i)).toBeVisible(); + await dismissNotification(page, /phone has been updated/i); await expect(page.locator('input[id="phone"]')).toHaveValue(newPhone); }); } @@ -194,9 +207,11 @@ export async function updateUserPassword( const passwordSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Password' }) }); + await passwordSection.locator('#newPassword').fill(newPassword); await passwordSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/password has been updated/i)).toBeVisible(); + await dismissNotification(page, /password has been updated/i); }); } @@ -215,6 +230,7 @@ export async function updateUserStatus( await button.waitFor({ state: 'visible', timeout: 10000 }); await button.click(); await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); + await dismissNotification(page, /has been (blocked|unblocked)/i); // Now verify the badge appears/disappears in the status section const statusSection = page.locator('[data-user-status]'); @@ -244,6 +260,7 @@ export async function updateUserLabels( await tagsInput.scrollIntoViewIfNeeded(); const existingTags = labelsSection.locator('[role="button"]').filter({ hasText: /×/i }); + const count = await existingTags.count(); for (let i = 0; i < count; i++) { await existingTags.first().click(); @@ -258,9 +275,12 @@ export async function updateUserLabels( await expect(updateButton).toBeEnabled({ timeout: 5000 }); await updateButton.click(); await expect(page.getByText(/have been updated/i)).toBeVisible(); + await dismissNotification(page, /have been updated/i); + const reloadedLabelsSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Labels' }) }); + for (const label of labels) { await expect(reloadedLabelsSection.getByText(label)).toBeVisible(); } @@ -286,6 +306,7 @@ export async function updateUserEmailVerification( .locator('ul.drop-list li.drop-list-item') .filter({ hasText: /(Verify|Unverify) email/ }) .locator('button'); + await dropdownItem.click({ force: true }); }); } @@ -309,6 +330,7 @@ export async function updateUserPhoneVerification( .locator('ul.drop-list li.drop-list-item') .filter({ hasText: /(Verify|Unverify) phone/ }) .locator('button'); + await dropdownItem.click({ force: true }); }); } @@ -347,6 +369,7 @@ export async function updateUserPrefs( await prefsSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/Preferences have been updated/i)).toBeVisible(); + await dismissNotification(page, /Preferences have been updated/i); }); } @@ -371,11 +394,15 @@ export async function updateUserMfa( await mfaToggle.click(); await mfaSection.getByRole('button', { name: 'Update' }).click(); await expect(page.getByText(/Multi-factor authentication has been/i)).toBeVisible(); + await dismissNotification(page, /Multi-factor authentication has been/i); } + const reloadedMfaSection = page.locator('form').filter({ has: page.getByRole('heading', { name: 'Multi-factor authentication' }) }); + const verifyToggle = reloadedMfaSection.getByRole('switch'); + if (enable) { await expect(verifyToggle).toHaveAttribute('aria-checked', 'true'); } else { diff --git a/package.json b/package.json index 8d45856dc6..991a9ab205 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "TZ=EST vitest run", "test:ui": "TZ=EST vitest --ui", "test:watch": "TZ=EST vitest watch", - "e2e": "playwright test", + "e2e": "playwright test --reporter=list", "e2e:ui": "playwright test --ui" }, "dependencies": { From a83f27b0c8d3e446a1f1c11704fda5c1b6b89274 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 13:06:07 +0530 Subject: [PATCH 09/19] ci. --- .github/workflows/e2e.yml | 6 +++--- .github/workflows/tests.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d974469ee5..e9b4fe09f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,15 +13,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Cache Playwright browsers uses: actions/cache@v4 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7f699dd18..a1ae3077ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,15 +16,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Audit dependencies run: pnpm audit --audit-level high From 93b8daa62ed83184f176bd5fd8a7fbf6e6b9f7e1 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 13:58:19 +0530 Subject: [PATCH 10/19] tests. --- e2e/auth/navigation.ts | 2 +- e2e/auth/users.ts | 87 +++++++++++++++++++++++++++------- e2e/helpers/delete.ts | 3 +- e2e/journeys/auth-free.spec.ts | 13 +++-- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/e2e/auth/navigation.ts b/e2e/auth/navigation.ts index 033e3e5893..30daca3de0 100644 --- a/e2e/auth/navigation.ts +++ b/e2e/auth/navigation.ts @@ -42,7 +42,7 @@ export async function navigateToUser( await page.goto(buildAuthUrl(region, projectId, `/user-${userId}`)); await page.waitForURL(buildAuthUrlPattern(region, projectId, `/user-${userId}`)); - await page.locator('input[id="name"]').waitFor({ state: 'attached' }); + await page.locator('input[id="name"]').waitFor({ state: 'visible' }); }); } diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index e133c2a5f7..db02d6b349 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -22,8 +22,19 @@ export type UserPrefs = { async function dismissNotification(page: Page, messagePattern: RegExp): Promise { const notification = page.locator('.toast').filter({ hasText: messagePattern }); - await notification.getByRole('button').last().click(); - await expect(notification).not.toBeVisible(); + await expect(notification).toBeVisible({ timeout: 15000 }); + try { + const closeButtonByName = notification.getByRole('button', { name: /dismiss|close/i }); + if ((await closeButtonByName.count()) > 0) { + await closeButtonByName.first().click(); + } else { + await notification.getByRole('button').first().click(); + } + } catch { + await expect(notification).not.toBeVisible({ timeout: 10000 }); + return; + } + await expect(notification).not.toBeVisible({ timeout: 10000 }); } export async function createUser( @@ -101,6 +112,17 @@ export async function searchUser(page: Page, query: string): Promise { }); } +export async function clearUserSearch(page: Page): Promise { + return test.step('clear user search', async () => { + const searchInput = page.getByPlaceholder(/Search by name, email, phone, or ID/i); + await searchInput.clear(); + + // wait for URL to drop the search param + await expect(page).not.toHaveURL(/search=/); + await expect(searchInput).toHaveValue(''); + }); +} + export async function deleteUser( page: Page, region: string, @@ -142,8 +164,12 @@ export async function updateUserName( has: page.getByRole('heading', { name: 'Name' }) }); - await nameSection.locator('id=name').fill(newName); - await nameSection.getByRole('button', { name: 'Update' }).click(); + const nameInput = nameSection.locator('id=name'); + await nameInput.waitFor({ state: 'visible', timeout: 10000 }); + await nameInput.fill(newName); + const updateButton = nameSection.getByRole('button', { name: 'Update' }); + await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await updateButton.click(); await expect(page.getByText(/name has been updated/i)).toBeVisible(); await dismissNotification(page, /name has been updated/i); await expect(page.locator('input[id="name"]')).toHaveValue(newName); @@ -164,8 +190,12 @@ export async function updateUserEmail( has: page.getByRole('heading', { name: 'Email' }) }); - await emailSection.locator('id=email').fill(newEmail); - await emailSection.getByRole('button', { name: 'Update' }).click(); + const emailInput = emailSection.locator('id=email'); + await emailInput.waitFor({ state: 'visible', timeout: 10000 }); + await emailInput.fill(newEmail); + const updateButton = emailSection.getByRole('button', { name: 'Update' }); + await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await updateButton.click(); await expect(page.getByText(/email has been updated/i)).toBeVisible(); await dismissNotification(page, /email has been updated/i); await expect(page.locator('input[id="email"]')).toHaveValue(newEmail); @@ -186,8 +216,12 @@ export async function updateUserPhone( has: page.getByRole('heading', { name: 'Phone' }) }); - await phoneSection.locator('id=phone').fill(newPhone); - await phoneSection.getByRole('button', { name: 'Update' }).click(); + const phoneInput = phoneSection.locator('id=phone'); + await phoneInput.waitFor({ state: 'visible', timeout: 10000 }); + await phoneInput.fill(newPhone); + const updateButton = phoneSection.getByRole('button', { name: 'Update' }); + await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await updateButton.click(); await expect(page.getByText(/phone has been updated/i)).toBeVisible(); await dismissNotification(page, /phone has been updated/i); await expect(page.locator('input[id="phone"]')).toHaveValue(newPhone); @@ -208,8 +242,12 @@ export async function updateUserPassword( has: page.getByRole('heading', { name: 'Password' }) }); - await passwordSection.locator('#newPassword').fill(newPassword); - await passwordSection.getByRole('button', { name: 'Update' }).click(); + const passwordInput = passwordSection.locator('#newPassword'); + await passwordInput.waitFor({ state: 'visible', timeout: 10000 }); + await passwordInput.fill(newPassword); + const updateButton = passwordSection.getByRole('button', { name: 'Update' }); + await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await updateButton.click(); await expect(page.getByText(/password has been updated/i)).toBeVisible(); await dismissNotification(page, /password has been updated/i); }); @@ -298,16 +336,22 @@ export async function updateUserEmailVerification( await navigateToUser(page, region, projectId, userId); const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); + await verifyButton.waitFor({ state: 'visible', timeout: 10000 }); await verifyButton.click(); - await page.locator('ul.drop-list').waitFor({ state: 'visible' }); + const dropList = page.locator('ul.drop-list'); + await dropList.waitFor({ state: 'visible', timeout: 10000 }); - const dropdownItem = page - .locator('ul.drop-list li.drop-list-item') + const dropdownItem = dropList + .locator('li.drop-list-item') .filter({ hasText: /(Verify|Unverify) email/ }) .locator('button'); - await dropdownItem.click({ force: true }); + await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); + await dropdownItem.click(); + + await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ timeout: 15000 }); + await dismissNotification(page, /has been (verified|unverified)/i); }); } @@ -322,16 +366,23 @@ export async function updateUserPhoneVerification( await navigateToUser(page, region, projectId, userId); const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); + await verifyButton.waitFor({ state: 'visible', timeout: 10000 }); await verifyButton.click(); - await page.locator('ul.drop-list').waitFor({ state: 'visible' }); + const dropList = page.locator('ul.drop-list'); + await dropList.waitFor({ state: 'visible', timeout: 10000 }); - const dropdownItem = page - .locator('ul.drop-list li.drop-list-item') + const dropdownItem = dropList + .locator('li.drop-list-item') .filter({ hasText: /(Verify|Unverify) phone/ }) .locator('button'); - await dropdownItem.click({ force: true }); + await expect(dropdownItem).toBeVisible({ timeout: 10000 }); + await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); + await dropdownItem.click(); + + await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ timeout: 15000 }); + await dismissNotification(page, /has been (verified|unverified)/i); }); } diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index 58cee205d2..2715c587ff 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -75,9 +75,8 @@ export async function deleteAccount(page: Page, maxRetries = 3) { console.log( `Attempt ${attempt + 1}: Account deletion failed due to active memberships. Retrying...` ); - // close the dialog if still open + await page.keyboard.press('Escape').catch(() => {}); - // wait before retrying (org deletion might still be processing) await page.waitForTimeout(2000); continue; } diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts index a376fb7ab3..6f4a39331d 100644 --- a/e2e/journeys/auth-free.spec.ts +++ b/e2e/journeys/auth-free.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from '../fixtures/base'; import { createUser, searchUser, + clearUserSearch, updateUserName, updateUserEmail, updateUserPhone, @@ -80,10 +81,16 @@ test('auth flow - free tier', async ({ page, project }) => { }); await test.step('verify multiple users', async () => { + await clearUserSearch(page); await navigateToUsers(page, project.region, project.id); - await expect(page.getByText('Updated Test User')).toBeVisible(); - await expect(page.getByText('Second User')).toBeVisible(); - await expect(page.getByText('Third User')).toBeVisible(); + + const updatedRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); + const secondRow = page.locator('[role="row"]').filter({ hasText: 'Second User' }); + const thirdRow = page.locator('[role="row"]').filter({ hasText: 'Third User' }); + + await expect(updatedRow).toBeVisible({ timeout: 5000 }); + await expect(secondRow).toBeVisible({ timeout: 5000 }); + await expect(thirdRow).toBeVisible({ timeout: 5000 }); }); await test.step('test email and phone verification', async () => { From 93ff2d5605cb0866c261f38694158d496a6d272c Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:16:43 +0530 Subject: [PATCH 11/19] tests. --- e2e/auth/users.ts | 10 +++++++--- e2e/helpers/delete.ts | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index db02d6b349..b1d10118c0 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -139,7 +139,7 @@ export async function deleteUser( await dialog.waitFor({ state: 'visible' }); await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); - await page.waitForURL(buildAuthUrlPattern(region, projectId, '$')); + await page.waitForURL(buildAuthUrlPattern(region, projectId, '(/\\?.*)?$')); await expect(page.getByText(/has been deleted/i)).toBeVisible(); await dismissNotification(page, /has been deleted/i); await searchUser(page, userId); @@ -350,7 +350,9 @@ export async function updateUserEmailVerification( await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); await dropdownItem.click(); - await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ timeout: 15000 }); + await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ + timeout: 15000 + }); await dismissNotification(page, /has been (verified|unverified)/i); }); } @@ -381,7 +383,9 @@ export async function updateUserPhoneVerification( await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); await dropdownItem.click(); - await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ timeout: 15000 }); + await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ + timeout: 15000 + }); await dismissNotification(page, /has been (verified|unverified)/i); }); } diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index 2715c587ff..b5ab19062c 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -56,14 +56,27 @@ export async function deleteAccount(page: Page, maxRetries = 3) { await page.goto('./account'); // click the Delete button in the CardGrid actions section - await page.getByRole('button', { name: 'Delete', exact: true }).click(); + const trigger = page.getByRole('button', { name: 'Delete', exact: true }); + await trigger.waitFor({ state: 'visible', timeout: 10000 }); + await trigger.click(); // wait for confirm modal to open const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible(); - - // click the confirm button in the modal (no name typing required) - await dialog.getByRole('button', { name: 'Delete', exact: true }).click(); + await expect(dialog).toBeVisible({ timeout: 10000 }); + + // click the confirm button in the modal + const confirm = dialog.getByRole('button', { name: 'Delete', exact: true }); + await confirm.waitFor({ state: 'attached', timeout: 5000 }); + await expect(confirm).toBeVisible({ timeout: 10000 }); + await expect(confirm).toBeEnabled({ timeout: 10000 }); + try { + await confirm.click({ timeout: 5000 }); + } catch { + const retryConfirm = dialog.getByRole('button', { name: 'Delete', exact: true }); + await retryConfirm.waitFor({ state: 'attached', timeout: 5000 }); + await expect(retryConfirm).toBeEnabled({ timeout: 10000 }); + await retryConfirm.click({ timeout: 5000 }); + } // check if we got an error about active memberships const membershipError = page.getByText(/active memberships/i); @@ -77,9 +90,21 @@ export async function deleteAccount(page: Page, maxRetries = 3) { ); await page.keyboard.press('Escape').catch(() => {}); - await page.waitForTimeout(2000); + await expect(dialog).toBeHidden({ timeout: 10000 }); + await page.waitForTimeout(1000); continue; } + + const successToast = page.getByText(/Account was deleted/i); + try { + await Promise.race([ + expect(successToast).toBeVisible({ timeout: 15000 }), + expect(dialog).toBeHidden({ timeout: 15000 }) + ]); + } catch { + await expect(dialog).toBeHidden({ timeout: 5000 }); + } + return; } From 3ae497bbfe3d2d164dc4d8832c924552a53f745a Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:22:57 +0530 Subject: [PATCH 12/19] update: acc. deletion toast. --- e2e/helpers/delete.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index b5ab19062c..864b883395 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -96,14 +96,7 @@ export async function deleteAccount(page: Page, maxRetries = 3) { } const successToast = page.getByText(/Account was deleted/i); - try { - await Promise.race([ - expect(successToast).toBeVisible({ timeout: 15000 }), - expect(dialog).toBeHidden({ timeout: 15000 }) - ]); - } catch { - await expect(dialog).toBeHidden({ timeout: 5000 }); - } + await successToast.isVisible({ timeout: 5000 }).catch(() => false); return; } From 1d7cc0955e534200b19f1dd34d4668ee49d52acc Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:29:38 +0530 Subject: [PATCH 13/19] no retry for now. --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index fe6952ed6a..d035c4fe32 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ const config: PlaywrightTestConfig = { timeout: 120000, reportSlowTests: null, reporter: [['html', { open: 'never' }]], - retries: 3, + // retries: 3, testDir: 'e2e', use: { baseURL: 'http://localhost:3000/console/', From 098f4ca225638162a0a72b5f6e1c5d1e78222f4f Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:37:46 +0530 Subject: [PATCH 14/19] test: blacksmith. --- .github/workflows/e2e.yml | 2 +- e2e/auth/users.ts | 24 +++++++++++++++++------- package.json | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e9b4fe09f9..fc88ff6b0e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,7 +9,7 @@ on: jobs: e2e: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v4 diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index b1d10118c0..0a763dbb1b 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -335,19 +335,24 @@ export async function updateUserEmailVerification( return test.step(`update user email verification to: ${shouldVerify}`, async () => { await navigateToUser(page, region, projectId, userId); + // Ensure the actions area is rendered (anchor on block/unblock button) + const actionsAnchor = page.getByRole('button', { name: /Block account|Unblock account/ }); + await actionsAnchor.waitFor({ state: 'visible', timeout: 15000 }); + await actionsAnchor.scrollIntoViewIfNeeded(); + const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); - await verifyButton.waitFor({ state: 'visible', timeout: 10000 }); + await verifyButton.waitFor({ state: 'visible', timeout: 15000 }); await verifyButton.click(); const dropList = page.locator('ul.drop-list'); - await dropList.waitFor({ state: 'visible', timeout: 10000 }); + await dropList.waitFor({ state: 'visible', timeout: 15000 }); const dropdownItem = dropList .locator('li.drop-list-item') .filter({ hasText: /(Verify|Unverify) email/ }) .locator('button'); - await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); + await expect(dropdownItem).toBeEnabled({ timeout: 15000 }); await dropdownItem.click(); await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ @@ -367,20 +372,25 @@ export async function updateUserPhoneVerification( return test.step(`update user phone verification to: ${shouldVerify}`, async () => { await navigateToUser(page, region, projectId, userId); + // Ensure the actions area is rendered (anchor on block/unblock button) + const actionsAnchor = page.getByRole('button', { name: /Block account|Unblock account/ }); + await actionsAnchor.waitFor({ state: 'visible', timeout: 15000 }); + await actionsAnchor.scrollIntoViewIfNeeded(); + const verifyButton = page.getByRole('button', { name: /Verify account|Unverify account/ }); - await verifyButton.waitFor({ state: 'visible', timeout: 10000 }); + await verifyButton.waitFor({ state: 'visible', timeout: 15000 }); await verifyButton.click(); const dropList = page.locator('ul.drop-list'); - await dropList.waitFor({ state: 'visible', timeout: 10000 }); + await dropList.waitFor({ state: 'visible', timeout: 15000 }); const dropdownItem = dropList .locator('li.drop-list-item') .filter({ hasText: /(Verify|Unverify) phone/ }) .locator('button'); - await expect(dropdownItem).toBeVisible({ timeout: 10000 }); - await expect(dropdownItem).toBeEnabled({ timeout: 10000 }); + await expect(dropdownItem).toBeVisible({ timeout: 15000 }); + await expect(dropdownItem).toBeEnabled({ timeout: 15000 }); await dropdownItem.click(); await expect(page.getByText(/has been (verified|unverified)/i)).toBeVisible({ diff --git a/package.json b/package.json index 991a9ab205..307eb6fde9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf node_modules && rm -rf .svelte_kit && pnpm i --force", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", + "format": "prettier --write --cache .", "lint": "prettier --check . && eslint .", "test": "TZ=EST vitest run", "test:ui": "TZ=EST vitest --ui", From 27a677df139d5cd64addcf347e5902d40ccef3cd Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:44:21 +0530 Subject: [PATCH 15/19] revert: blacksmith, doesn't seem to pick up the job. --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fc88ff6b0e..e9b4fe09f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,7 +9,7 @@ on: jobs: e2e: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 23e8adb1d0872224f1f1a17284e269e8ae12c6ed Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 1 Nov 2025 14:47:29 +0530 Subject: [PATCH 16/19] revert: add retry. tests are extremely flaky! --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index d035c4fe32..fe6952ed6a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ const config: PlaywrightTestConfig = { timeout: 120000, reportSlowTests: null, reporter: [['html', { open: 'never' }]], - // retries: 3, + retries: 3, testDir: 'e2e', use: { baseURL: 'http://localhost:3000/console/', From 0d173a54248587b64e94de461555522bab651ab6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 3 Nov 2025 13:05:16 +0530 Subject: [PATCH 17/19] bump: timeouts, attempt to see if this helps on staging! --- e2e/auth/users.ts | 22 +++++++++++----------- e2e/helpers/delete.ts | 12 ++++++------ e2e/journeys/auth-free.spec.ts | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index 0a763dbb1b..8431c95283 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -31,10 +31,10 @@ async function dismissNotification(page: Page, messagePattern: RegExp): Promise< await notification.getByRole('button').first().click(); } } catch { - await expect(notification).not.toBeVisible({ timeout: 10000 }); + await expect(notification).not.toBeVisible({ timeout: 15000 }); return; } - await expect(notification).not.toBeVisible({ timeout: 10000 }); + await expect(notification).not.toBeVisible({ timeout: 15000 }); } export async function createUser( @@ -165,10 +165,10 @@ export async function updateUserName( }); const nameInput = nameSection.locator('id=name'); - await nameInput.waitFor({ state: 'visible', timeout: 10000 }); + await nameInput.waitFor({ state: 'visible', timeout: 15000 }); await nameInput.fill(newName); const updateButton = nameSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/name has been updated/i)).toBeVisible(); await dismissNotification(page, /name has been updated/i); @@ -191,10 +191,10 @@ export async function updateUserEmail( }); const emailInput = emailSection.locator('id=email'); - await emailInput.waitFor({ state: 'visible', timeout: 10000 }); + await emailInput.waitFor({ state: 'visible', timeout: 15000 }); await emailInput.fill(newEmail); const updateButton = emailSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/email has been updated/i)).toBeVisible(); await dismissNotification(page, /email has been updated/i); @@ -217,10 +217,10 @@ export async function updateUserPhone( }); const phoneInput = phoneSection.locator('id=phone'); - await phoneInput.waitFor({ state: 'visible', timeout: 10000 }); + await phoneInput.waitFor({ state: 'visible', timeout: 15000 }); await phoneInput.fill(newPhone); const updateButton = phoneSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/phone has been updated/i)).toBeVisible(); await dismissNotification(page, /phone has been updated/i); @@ -243,10 +243,10 @@ export async function updateUserPassword( }); const passwordInput = passwordSection.locator('#newPassword'); - await passwordInput.waitFor({ state: 'visible', timeout: 10000 }); + await passwordInput.waitFor({ state: 'visible', timeout: 15000 }); await passwordInput.fill(newPassword); const updateButton = passwordSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 10000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/password has been updated/i)).toBeVisible(); await dismissNotification(page, /password has been updated/i); @@ -265,7 +265,7 @@ export async function updateUserStatus( const buttonText = enabled ? 'Unblock account' : 'Block account'; const button = page.getByRole('button', { name: buttonText }); - await button.waitFor({ state: 'visible', timeout: 10000 }); + await button.waitFor({ state: 'visible', timeout: 15000 }); await button.click(); await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); await dismissNotification(page, /has been (blocked|unblocked)/i); diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index 864b883395..f473e799ef 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -57,24 +57,24 @@ export async function deleteAccount(page: Page, maxRetries = 3) { // click the Delete button in the CardGrid actions section const trigger = page.getByRole('button', { name: 'Delete', exact: true }); - await trigger.waitFor({ state: 'visible', timeout: 10000 }); + await trigger.waitFor({ state: 'visible', timeout: 15000 }); await trigger.click(); // wait for confirm modal to open const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible({ timeout: 10000 }); + await expect(dialog).toBeVisible({ timeout: 15000 }); // click the confirm button in the modal const confirm = dialog.getByRole('button', { name: 'Delete', exact: true }); await confirm.waitFor({ state: 'attached', timeout: 5000 }); - await expect(confirm).toBeVisible({ timeout: 10000 }); - await expect(confirm).toBeEnabled({ timeout: 10000 }); + await expect(confirm).toBeVisible({ timeout: 15000 }); + await expect(confirm).toBeEnabled({ timeout: 15000 }); try { await confirm.click({ timeout: 5000 }); } catch { const retryConfirm = dialog.getByRole('button', { name: 'Delete', exact: true }); await retryConfirm.waitFor({ state: 'attached', timeout: 5000 }); - await expect(retryConfirm).toBeEnabled({ timeout: 10000 }); + await expect(retryConfirm).toBeEnabled({ timeout: 15000 }); await retryConfirm.click({ timeout: 5000 }); } @@ -90,7 +90,7 @@ export async function deleteAccount(page: Page, maxRetries = 3) { ); await page.keyboard.press('Escape').catch(() => {}); - await expect(dialog).toBeHidden({ timeout: 10000 }); + await expect(dialog).toBeHidden({ timeout: 15000 }); await page.waitForTimeout(1000); continue; } diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts index 6f4a39331d..d9f220045b 100644 --- a/e2e/journeys/auth-free.spec.ts +++ b/e2e/journeys/auth-free.spec.ts @@ -53,14 +53,14 @@ test('auth flow - free tier', async ({ page, project }) => { await test.step('verify blocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).toBeVisible({ timeout: 10000 }); + await expect(userRow.getByText('blocked')).toBeVisible({ timeout: 15000 }); }); await updateUserStatus(page, project.region, project.id, user.id, true); await test.step('verify unblocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).not.toBeVisible({ timeout: 10000 }); + await expect(userRow.getByText('blocked')).not.toBeVisible({ timeout: 15000 }); }); await updateUserLabels(page, project.region, project.id, user.id, ['test', 'e2e', 'freeTier']); From d1ab0fa7d609a6a873b1341f18ca1c5e0a377a7d Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 15 Nov 2025 12:25:02 +0530 Subject: [PATCH 18/19] update: misc changes. --- e2e/auth/users.ts | 22 +++++++++++----------- e2e/helpers/delete.ts | 25 ++++++++++++++----------- e2e/helpers/region.ts | 13 +++++++++---- e2e/journeys/auth-free.spec.ts | 4 ++-- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/e2e/auth/users.ts b/e2e/auth/users.ts index 8431c95283..0e2757a83c 100644 --- a/e2e/auth/users.ts +++ b/e2e/auth/users.ts @@ -31,10 +31,10 @@ async function dismissNotification(page: Page, messagePattern: RegExp): Promise< await notification.getByRole('button').first().click(); } } catch { - await expect(notification).not.toBeVisible({ timeout: 15000 }); + await expect(notification).not.toBeVisible({ timeout: 15000 }); return; } - await expect(notification).not.toBeVisible({ timeout: 15000 }); + await expect(notification).not.toBeVisible({ timeout: 15000 }); } export async function createUser( @@ -165,10 +165,10 @@ export async function updateUserName( }); const nameInput = nameSection.locator('id=name'); - await nameInput.waitFor({ state: 'visible', timeout: 15000 }); + await nameInput.waitFor({ state: 'visible', timeout: 15000 }); await nameInput.fill(newName); const updateButton = nameSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 15000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/name has been updated/i)).toBeVisible(); await dismissNotification(page, /name has been updated/i); @@ -191,10 +191,10 @@ export async function updateUserEmail( }); const emailInput = emailSection.locator('id=email'); - await emailInput.waitFor({ state: 'visible', timeout: 15000 }); + await emailInput.waitFor({ state: 'visible', timeout: 15000 }); await emailInput.fill(newEmail); const updateButton = emailSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 15000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/email has been updated/i)).toBeVisible(); await dismissNotification(page, /email has been updated/i); @@ -217,10 +217,10 @@ export async function updateUserPhone( }); const phoneInput = phoneSection.locator('id=phone'); - await phoneInput.waitFor({ state: 'visible', timeout: 15000 }); + await phoneInput.waitFor({ state: 'visible', timeout: 15000 }); await phoneInput.fill(newPhone); const updateButton = phoneSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 15000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/phone has been updated/i)).toBeVisible(); await dismissNotification(page, /phone has been updated/i); @@ -243,10 +243,10 @@ export async function updateUserPassword( }); const passwordInput = passwordSection.locator('#newPassword'); - await passwordInput.waitFor({ state: 'visible', timeout: 15000 }); + await passwordInput.waitFor({ state: 'visible', timeout: 15000 }); await passwordInput.fill(newPassword); const updateButton = passwordSection.getByRole('button', { name: 'Update' }); - await expect(updateButton).toBeEnabled({ timeout: 15000 }); + await expect(updateButton).toBeEnabled({ timeout: 15000 }); await updateButton.click(); await expect(page.getByText(/password has been updated/i)).toBeVisible(); await dismissNotification(page, /password has been updated/i); @@ -265,7 +265,7 @@ export async function updateUserStatus( const buttonText = enabled ? 'Unblock account' : 'Block account'; const button = page.getByRole('button', { name: buttonText }); - await button.waitFor({ state: 'visible', timeout: 15000 }); + await button.waitFor({ state: 'visible', timeout: 15000 }); await button.click(); await expect(page.getByText(/has been (blocked|unblocked)/i)).toBeVisible(); await dismissNotification(page, /has been (blocked|unblocked)/i); diff --git a/e2e/helpers/delete.ts b/e2e/helpers/delete.ts index f473e799ef..3aa55d8b65 100644 --- a/e2e/helpers/delete.ts +++ b/e2e/helpers/delete.ts @@ -1,4 +1,4 @@ -import { test, type Page, expect } from '@playwright/test'; +import { test, type Page } from '@playwright/test'; export async function deleteProject(page: Page, region: string, projectId: string) { return test.step('delete project', async () => { @@ -12,7 +12,8 @@ export async function deleteProject(page: Page, region: string, projectId: strin // Wait for modal to open const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible(); + await dialog.waitFor({ state: 'visible', timeout: 5000 }); + // await expect(dialog).toBeVisible(); // Type the project name to confirm await dialog.locator('#project-name').fill(projectName?.trim() || ''); @@ -37,7 +38,8 @@ export async function deleteOrganization(page: Page, organizationId: string) { // Wait for modal to open const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible(); + await dialog.waitFor({ state: 'visible', timeout: 5000 }); + // await expect(dialog).toBeVisible(); // Type the organization name to confirm await dialog.locator('#organization-name').fill(organizationName?.trim() || ''); @@ -57,24 +59,25 @@ export async function deleteAccount(page: Page, maxRetries = 3) { // click the Delete button in the CardGrid actions section const trigger = page.getByRole('button', { name: 'Delete', exact: true }); - await trigger.waitFor({ state: 'visible', timeout: 15000 }); + await trigger.waitFor({ state: 'visible', timeout: 15000 }); await trigger.click(); // wait for confirm modal to open const dialog = page.locator('dialog[open]'); - await expect(dialog).toBeVisible({ timeout: 15000 }); + await dialog.waitFor({ state: 'visible', timeout: 5000 }); + // await expect(dialog).toBeVisible({ timeout: 15000 }); // click the confirm button in the modal const confirm = dialog.getByRole('button', { name: 'Delete', exact: true }); - await confirm.waitFor({ state: 'attached', timeout: 5000 }); - await expect(confirm).toBeVisible({ timeout: 15000 }); - await expect(confirm).toBeEnabled({ timeout: 15000 }); + await confirm.waitFor({ state: 'visible', timeout: 5000 }); + // await expect(confirm).toBeVisible({ timeout: 15000 }); + // await expect(confirm).toBeEnabled({ timeout: 15000 }); try { await confirm.click({ timeout: 5000 }); } catch { const retryConfirm = dialog.getByRole('button', { name: 'Delete', exact: true }); - await retryConfirm.waitFor({ state: 'attached', timeout: 5000 }); - await expect(retryConfirm).toBeEnabled({ timeout: 15000 }); + await retryConfirm.waitFor({ state: 'visible', timeout: 5000 }); + // await expect(retryConfirm).toBeEnabled({ timeout: 15000 }); await retryConfirm.click({ timeout: 5000 }); } @@ -90,7 +93,7 @@ export async function deleteAccount(page: Page, maxRetries = 3) { ); await page.keyboard.press('Escape').catch(() => {}); - await expect(dialog).toBeHidden({ timeout: 15000 }); + // await expect(dialog).toBeHidden({ timeout: 15000 }); await page.waitForTimeout(1000); continue; } diff --git a/e2e/helpers/region.ts b/e2e/helpers/region.ts index 0c5a6775f6..e6defd1725 100644 --- a/e2e/helpers/region.ts +++ b/e2e/helpers/region.ts @@ -7,10 +7,15 @@ export async function selectRandomRegion(page: Page, dialog: Locator): Promise 0) { const randomIndex = Math.floor(Math.random() * options.length); diff --git a/e2e/journeys/auth-free.spec.ts b/e2e/journeys/auth-free.spec.ts index d9f220045b..13dc4ec2cf 100644 --- a/e2e/journeys/auth-free.spec.ts +++ b/e2e/journeys/auth-free.spec.ts @@ -53,14 +53,14 @@ test('auth flow - free tier', async ({ page, project }) => { await test.step('verify blocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).toBeVisible({ timeout: 15000 }); + await expect(userRow.getByText('blocked')).toBeVisible({ timeout: 15000 }); }); await updateUserStatus(page, project.region, project.id, user.id, true); await test.step('verify unblocked status', async () => { await navigateToUsers(page, project.region, project.id); const userRow = page.locator('[role="row"]').filter({ hasText: 'Updated Test User' }); - await expect(userRow.getByText('blocked')).not.toBeVisible({ timeout: 15000 }); + await expect(userRow.getByText('blocked')).not.toBeVisible({ timeout: 15000 }); }); await updateUserLabels(page, project.region, project.id, user.id, ['test', 'e2e', 'freeTier']); From 5a657d054dbe844aeccedde1b9c30649bc47ee49 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 18 Nov 2025 17:12:52 +0530 Subject: [PATCH 19/19] lint. --- .github/workflows/dockerize-profiles.yml | 84 ++++++++++++------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/.github/workflows/dockerize-profiles.yml b/.github/workflows/dockerize-profiles.yml index 8e09a234a4..aafa95f324 100644 --- a/.github/workflows/dockerize-profiles.yml +++ b/.github/workflows/dockerize-profiles.yml @@ -1,49 +1,49 @@ name: Dockerize Profiles on: - push: - branches: [feat-profiles] - pull_request: - types: [opened, synchronize, reopened] - branches: [feat-profiles] - workflow_dispatch: + push: + branches: [feat-profiles] + pull_request: + types: [opened, synchronize, reopened] + branches: [feat-profiles] + workflow_dispatch: jobs: - dockerize-profiles: - runs-on: ubuntu-latest - - steps: - - name: Checkout the repo - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerize-profiles: + runs-on: ubuntu-latest - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: appwrite/console-profiles - tags: | - type=ref,event=branch,prefix=branch- - type=ref,event=pr - type=sha,prefix=sha- - type=raw,value=gh-${{ github.run_id}} - flavor: | - latest=false + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: appwrite/console-profiles + tags: | + type=ref,event=branch,prefix=branch- + type=ref,event=pr + type=sha,prefix=sha- + type=raw,value=gh-${{ github.run_id}} + flavor: | + latest=false + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }}