Refactor E2E tests, update utils, and modify plan-picker validation
Several changes have been made to optimize the testing and validation workflows. The end-to-end tests for user billing, invitations, account, team billing, and team accounts have been retouched for improved performance and better accuracy. There has been a minor modification in the validation rules for plan-picker in the billing component. Furthermore, modifications are made to several utility functions to fine-tune their operations.
This commit is contained in:
@@ -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,21 +21,57 @@ 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() {
|
||||
|
||||
@@ -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,8 +52,10 @@ 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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -150,7 +150,11 @@ export function AccountSelector({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-full p-0" collisionPadding={20}>
|
||||
<PopoverContent
|
||||
data-test={'account-selector-content'}
|
||||
className="w-full p-0"
|
||||
collisionPadding={20}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchAccount')} className="h-9" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user