diff --git a/apps/e2e/tests/account/account.po.ts b/apps/e2e/tests/account/account.po.ts index 43956c28c..0c774e900 100644 --- a/apps/e2e/tests/account/account.po.ts +++ b/apps/e2e/tests/account/account.po.ts @@ -1,4 +1,5 @@ -import { Page } from '@playwright/test'; +import { Page, expect } from '@playwright/test'; + import { AuthPageObject } from '../authentication/auth.po'; export class AccountPageObject { @@ -20,24 +21,60 @@ export class AccountPageObject { } async updateEmail(email: string) { - await this.page.fill('[data-test="account-email-form-email-input"]', email); - await this.page.fill('[data-test="account-email-form-repeat-email-input"]', email); - await this.page.click('[data-test="account-email-form"] button'); + await expect(async () => { + await this.page.fill( + '[data-test="account-email-form-email-input"]', + email, + ); + + await this.page.fill( + '[data-test="account-email-form-repeat-email-input"]', + email, + ); + + await this.page.click('[data-test="account-email-form"] button'); + + const req = await this.page.waitForResponse((resp) => { + return resp.url().includes('auth/v1/user'); + }); + + expect(req.status()).toBe(200); + }).toPass(); } async updatePassword(password: string) { - await this.page.fill('[data-test="account-password-form-password-input"]', password); - await this.page.fill('[data-test="account-password-form-repeat-password-input"]', password); + await this.page.fill( + '[data-test="account-password-form-password-input"]', + password, + ); + await this.page.fill( + '[data-test="account-password-form-repeat-password-input"]', + password, + ); await this.page.click('[data-test="account-password-form"] button'); } async deleteAccount() { - await this.page.click('[data-test="delete-account-button"]'); - await this.page.fill('[data-test="delete-account-input-field"]', 'DELETE'); - await this.page.click('[data-test="confirm-delete-account-button"]'); + await expect(async () => { + await this.page.click('[data-test="delete-account-button"]'); + await this.page.fill( + '[data-test="delete-account-input-field"]', + 'DELETE', + ); + await this.page.click('[data-test="confirm-delete-account-button"]'); + + const response = await this.page.waitForResponse((resp) => { + return ( + resp.url().includes('home/settings') && + resp.request().method() === 'POST' + ); + }); + + expect(response.status()).toBe(204); + }).toPass(); } getProfileName() { return this.page.locator('[data-test="account-dropdown-display-name"]'); } -} \ No newline at end of file +} diff --git a/apps/e2e/tests/account/account.spec.ts b/apps/e2e/tests/account/account.spec.ts index 742d369ac..f743c5114 100644 --- a/apps/e2e/tests/account/account.spec.ts +++ b/apps/e2e/tests/account/account.spec.ts @@ -1,4 +1,5 @@ -import { expect, Page, test } from '@playwright/test'; +import { Page, expect, test } from '@playwright/test'; + import { AccountPageObject } from './account.po'; test.describe('Account Settings', () => { @@ -28,12 +29,6 @@ test.describe('Account Settings', () => { const email = account.auth.createRandomEmail(); await account.updateEmail(email); - - const req = await page.waitForResponse((resp) => { - return resp.url().includes('auth/v1/user'); - }); - - expect(req.status()).toBe(200); }); test('user can update their password', async () => { @@ -57,11 +52,13 @@ test.describe('Account Deletion', () => { await account.deleteAccount(); const response = await page.waitForResponse((resp) => { - return resp.url().includes('home/settings') && - resp.request().method() === 'POST'; + return ( + resp.url().includes('home/settings') && + resp.request().method() === 'POST' + ); }); // The server should respond with a 303 redirect expect(response.status()).toBe(303); }); -}); \ No newline at end of file +}); diff --git a/apps/e2e/tests/invitations/invitations.po.ts b/apps/e2e/tests/invitations/invitations.po.ts index 80b38e9eb..4aaa293b4 100644 --- a/apps/e2e/tests/invitations/invitations.po.ts +++ b/apps/e2e/tests/invitations/invitations.po.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test'; +import { Page, expect } from '@playwright/test'; import { AuthPageObject } from '../authentication/auth.po'; import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po'; @@ -14,8 +14,8 @@ export class InvitationsPageObject { this.teamAccounts = new TeamAccountsPageObject(page); } - async setup() { - await this.teamAccounts.setup(); + setup() { + return this.teamAccounts.setup(); } public async inviteMembers( @@ -41,9 +41,11 @@ export class InvitationsPageObject { `[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="invite-email-input"]`, invite.email, ); + await this.page.click( `[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`, ); + await this.page.click(`[data-test="role-option-${invite.role}"]`); if (index < invites.length - 1) { @@ -62,10 +64,12 @@ export class InvitationsPageObject { .click(); } - openInviteForm() { - return this.page - .locator('[data-test="invite-members-form-trigger"]') - .click(); + async openInviteForm() { + await expect(async () => { + await this.page.click('[data-test="invite-members-form-trigger"]'); + + return await expect(this.getInviteForm()).toBeVisible(); + }).toPass(); } getInvitations() { diff --git a/apps/e2e/tests/invitations/invitations.spec.ts b/apps/e2e/tests/invitations/invitations.spec.ts index 6f317842d..5c36fc4bd 100644 --- a/apps/e2e/tests/invitations/invitations.spec.ts +++ b/apps/e2e/tests/invitations/invitations.spec.ts @@ -75,7 +75,6 @@ test.describe('Full Invitation Flow', () => { test('should invite users and let users accept an invite', async () => { await invitations.navigateToMembers(); - await invitations.openInviteForm(); const invites = [ { @@ -88,6 +87,7 @@ test.describe('Full Invitation Flow', () => { }, ]; + await invitations.openInviteForm(); await invitations.inviteMembers(invites); const firstEmail = invites[0]!.email; diff --git a/apps/e2e/tests/team-accounts/team-accounts.po.ts b/apps/e2e/tests/team-accounts/team-accounts.po.ts index c10b90039..0b639e401 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.po.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.po.ts @@ -13,19 +13,14 @@ export class TeamAccountsPageObject { async setup(params = this.createTeamName()) { await this.auth.signUpFlow('/home'); + await this.createTeam(params); } - getTeamFromSelector(teamSlug: string) { - return this.page.locator( - `[data-test="account-selector-team"][data-value="${teamSlug}"]`, - ); - } - - selectAccount(teamName: string) { - return this.page.click( - `[data-test="account-selector-team"][data-name="${teamName}"]`, - ); + getTeamFromSelector(teamName: string) { + return this.page.locator(`[data-test="account-selector-team"]`, { + hasText: teamName, + }); } getTeams() { @@ -48,8 +43,14 @@ export class TeamAccountsPageObject { .click(); } - openAccountsSelector() { - return this.page.click('[data-test="account-selector-trigger"]'); + async openAccountsSelector() { + await expect(async () => { + await this.page.click('[data-test="account-selector-trigger"]'); + + return expect( + this.page.locator('[data-test="account-selector-content"]'), + ).toBeVisible(); + }).toPass(); } async createTeam({ teamName, slug } = this.createTeamName()) { @@ -62,29 +63,41 @@ export class TeamAccountsPageObject { await this.page.waitForURL(`/home/${slug}`); } - async updateName(name: string) { - await this.page.fill( - '[data-test="update-team-account-name-form"] input', - name, - ); + async updateName(name: string, slug: string) { + await expect(async () => { + await this.page.fill( + '[data-test="update-team-account-name-form"] input', + name, + ); - await this.page.click('[data-test="update-team-account-name-form"] button'); + await this.page.click( + '[data-test="update-team-account-name-form"] button', + ); + + // the slug should be updated to match the new team name + await expect(this.page).toHaveURL( + `http://localhost:3000/home/${slug}/settings`, + ); + }).toPass(); } async deleteAccount(teamName: string) { - await this.page.click('[data-test="delete-team-trigger"]'); + await expect(async () => { + await this.page.click('[data-test="delete-team-trigger"]'); - expect( - await this.page - .locator('[data-test="delete-team-form-confirm-input"]') - .isVisible(), - ).toBeTruthy(); + await expect( + this.page.locator('[data-test="delete-team-form-confirm-input"]'), + ).toBeVisible(); - await this.page.fill( - '[data-test="delete-team-form-confirm-input"]', - teamName, - ); - await this.page.click('[data-test="delete-team-form-confirm-button"]'); + await this.page.fill( + '[data-test="delete-team-form-confirm-input"]', + teamName, + ); + + await this.page.click('[data-test="delete-team-form-confirm-button"]'); + + await this.page.waitForURL('http://localhost:3000/home'); + }).toPass(); } createTeamName() { diff --git a/apps/e2e/tests/team-accounts/team-accounts.spec.ts b/apps/e2e/tests/team-accounts/team-accounts.spec.ts index 2abbe2be4..4c6c5e158 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.spec.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.spec.ts @@ -17,14 +17,14 @@ test.describe('Team Accounts', () => { const { teamName, slug } = teamAccounts.createTeamName(); await teamAccounts.goToSettings(); - await teamAccounts.updateName(teamName); + await teamAccounts.updateName(teamName, slug); // the slug should be updated to match the new team name await page.waitForURL(`http://localhost:3000/home/${slug}/settings`); await teamAccounts.openAccountsSelector(); - await expect(teamAccounts.getTeamFromSelector(slug)).toBeVisible(); + await expect(teamAccounts.getTeamFromSelector(teamName)).toBeVisible(); }); }); @@ -40,7 +40,7 @@ test.describe('Account Deletion', () => { await teamAccounts.openAccountsSelector(); await expect( - teamAccounts.getTeamFromSelector(params.slug), + teamAccounts.getTeamFromSelector(params.teamName), ).not.toBeVisible(); }); }); diff --git a/apps/e2e/tests/team-billing/team-billing.spec.ts b/apps/e2e/tests/team-billing/team-billing.spec.ts index 300456082..2b33d5cb1 100644 --- a/apps/e2e/tests/team-billing/team-billing.spec.ts +++ b/apps/e2e/tests/team-billing/team-billing.spec.ts @@ -9,21 +9,21 @@ test.describe('Team Billing', () => { test.beforeAll(async ({ browser }) => { page = await browser.newPage(); po = new TeamBillingPageObject(page); - - await po.setup(); }); test('a team can subscribe to a plan', async () => { + await po.setup(); await po.teamAccounts.goToBilling(); await po.billing.selectPlan(0); await po.billing.proceedToCheckout(); + await po.billing.stripe.waitForForm(); await po.billing.stripe.fillForm(); await po.billing.stripe.submitForm(); await expect(po.billing.successStatus()).toBeVisible({ - timeout: 30000, + timeout: 25_000, }); await po.billing.returnToBilling(); diff --git a/apps/e2e/tests/user-billing/user-billing.spec.ts b/apps/e2e/tests/user-billing/user-billing.spec.ts index 66f7eb514..de604253e 100644 --- a/apps/e2e/tests/user-billing/user-billing.spec.ts +++ b/apps/e2e/tests/user-billing/user-billing.spec.ts @@ -17,11 +17,12 @@ test.describe('User Billing', () => { await po.billing.selectPlan(0); await po.billing.proceedToCheckout(); + await po.billing.stripe.waitForForm(); await po.billing.stripe.fillForm(); await po.billing.stripe.submitForm(); await expect(po.billing.successStatus()).toBeVisible({ - timeout: 30000, + timeout: 25_000, }); await po.billing.returnToBilling(); diff --git a/apps/e2e/tests/utils/billing.po.ts b/apps/e2e/tests/utils/billing.po.ts index d9d498651..569b2e264 100644 --- a/apps/e2e/tests/utils/billing.po.ts +++ b/apps/e2e/tests/utils/billing.po.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test'; +import { Page, expect } from '@playwright/test'; import { StripePageObject } from './stripe.po'; @@ -13,10 +13,17 @@ export class BillingPageObject { return this.page.locator('[data-test-plan]'); } - selectPlan(index: number = 0) { - const plans = this.plans(); + async selectPlan(index = 0) { + await expect(async () => { + const plans = this.plans(); + const plan = plans.nth(index); - return plans.nth(index).click(); + await expect(plan).toBeVisible(); + + await this.page.waitForTimeout(500); + + await plan.click(); + }).toPass(); } manageBillingButton() { diff --git a/apps/e2e/tests/utils/stripe.po.ts b/apps/e2e/tests/utils/stripe.po.ts index e9498600f..3a4bf4301 100644 --- a/apps/e2e/tests/utils/stripe.po.ts +++ b/apps/e2e/tests/utils/stripe.po.ts @@ -11,6 +11,12 @@ export class StripePageObject { return this.page.frameLocator('[name="embedded-checkout"]'); } + async waitForForm() { + return expect(async () => { + await expect(this.billingCountry()).toBeVisible(); + }).toPass(); + } + async fillForm(params: { billingName?: string; cardNumber?: string; @@ -18,10 +24,6 @@ export class StripePageObject { cvc?: string; billingCountry?: string; } = {}) { - expect(() => { - return this.getStripeCheckoutIframe().locator('form').isVisible(); - }); - const billingName = this.billingName(); const cardNumber = this.cardNumber(); const expiry = this.expiry(); @@ -55,10 +57,6 @@ export class StripePageObject { return this.getStripeCheckoutIframe().locator('#billingName'); } - cardForm() { - return this.getStripeCheckoutIframe().locator('form'); - } - billingCountry() { return this.getStripeCheckoutIframe().locator('#billingCountry'); } diff --git a/packages/billing/gateway/src/components/plan-picker.tsx b/packages/billing/gateway/src/components/plan-picker.tsx index 2cdd7ee91..1c95ec4cc 100644 --- a/packages/billing/gateway/src/components/plan-picker.tsx +++ b/packages/billing/gateway/src/components/plan-picker.tsx @@ -59,9 +59,9 @@ export function PlanPicker( resolver: zodResolver( z .object({ - planId: z.string().min(1), - productId: z.string().min(1), - interval: z.string().min(1), + planId: z.string(), + productId: z.string(), + interval: z.string(), }) .refine( (data) => { diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index f724fac10..81f8426b3 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -150,7 +150,11 @@ export function AccountSelector({ - +