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:
giancarlo
2024-05-05 18:15:11 +07:00
parent ca4932e351
commit b8f00f0a2d
12 changed files with 141 additions and 80 deletions

View File

@@ -1,4 +1,5 @@
import { Page } from '@playwright/test'; import { Page, expect } from '@playwright/test';
import { AuthPageObject } from '../authentication/auth.po'; import { AuthPageObject } from '../authentication/auth.po';
export class AccountPageObject { export class AccountPageObject {
@@ -20,24 +21,60 @@ export class AccountPageObject {
} }
async updateEmail(email: string) { async updateEmail(email: string) {
await this.page.fill('[data-test="account-email-form-email-input"]', email); await expect(async () => {
await this.page.fill('[data-test="account-email-form-repeat-email-input"]', email); await this.page.fill(
await this.page.click('[data-test="account-email-form"] button'); '[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) { async updatePassword(password: string) {
await this.page.fill('[data-test="account-password-form-password-input"]', password); await this.page.fill(
await this.page.fill('[data-test="account-password-form-repeat-password-input"]', password); '[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'); await this.page.click('[data-test="account-password-form"] button');
} }
async deleteAccount() { async deleteAccount() {
await this.page.click('[data-test="delete-account-button"]'); await expect(async () => {
await this.page.fill('[data-test="delete-account-input-field"]', 'DELETE'); await this.page.click('[data-test="delete-account-button"]');
await this.page.click('[data-test="confirm-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() { getProfileName() {
return this.page.locator('[data-test="account-dropdown-display-name"]'); return this.page.locator('[data-test="account-dropdown-display-name"]');
} }
} }

View File

@@ -1,4 +1,5 @@
import { expect, Page, test } from '@playwright/test'; import { Page, expect, test } from '@playwright/test';
import { AccountPageObject } from './account.po'; import { AccountPageObject } from './account.po';
test.describe('Account Settings', () => { test.describe('Account Settings', () => {
@@ -28,12 +29,6 @@ test.describe('Account Settings', () => {
const email = account.auth.createRandomEmail(); const email = account.auth.createRandomEmail();
await account.updateEmail(email); 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 () => { test('user can update their password', async () => {
@@ -57,11 +52,13 @@ test.describe('Account Deletion', () => {
await account.deleteAccount(); await account.deleteAccount();
const response = await page.waitForResponse((resp) => { const response = await page.waitForResponse((resp) => {
return resp.url().includes('home/settings') && return (
resp.request().method() === 'POST'; resp.url().includes('home/settings') &&
resp.request().method() === 'POST'
);
}); });
// The server should respond with a 303 redirect // The server should respond with a 303 redirect
expect(response.status()).toBe(303); expect(response.status()).toBe(303);
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Page } from '@playwright/test'; import { Page, expect } from '@playwright/test';
import { AuthPageObject } from '../authentication/auth.po'; import { AuthPageObject } from '../authentication/auth.po';
import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po'; import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po';
@@ -14,8 +14,8 @@ export class InvitationsPageObject {
this.teamAccounts = new TeamAccountsPageObject(page); this.teamAccounts = new TeamAccountsPageObject(page);
} }
async setup() { setup() {
await this.teamAccounts.setup(); return this.teamAccounts.setup();
} }
public async inviteMembers( public async inviteMembers(
@@ -41,9 +41,11 @@ export class InvitationsPageObject {
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="invite-email-input"]`, `[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="invite-email-input"]`,
invite.email, invite.email,
); );
await this.page.click( await this.page.click(
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`, `[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
); );
await this.page.click(`[data-test="role-option-${invite.role}"]`); await this.page.click(`[data-test="role-option-${invite.role}"]`);
if (index < invites.length - 1) { if (index < invites.length - 1) {
@@ -62,10 +64,12 @@ export class InvitationsPageObject {
.click(); .click();
} }
openInviteForm() { async openInviteForm() {
return this.page await expect(async () => {
.locator('[data-test="invite-members-form-trigger"]') await this.page.click('[data-test="invite-members-form-trigger"]');
.click();
return await expect(this.getInviteForm()).toBeVisible();
}).toPass();
} }
getInvitations() { getInvitations() {

View File

@@ -75,7 +75,6 @@ test.describe('Full Invitation Flow', () => {
test('should invite users and let users accept an invite', async () => { test('should invite users and let users accept an invite', async () => {
await invitations.navigateToMembers(); await invitations.navigateToMembers();
await invitations.openInviteForm();
const invites = [ const invites = [
{ {
@@ -88,6 +87,7 @@ test.describe('Full Invitation Flow', () => {
}, },
]; ];
await invitations.openInviteForm();
await invitations.inviteMembers(invites); await invitations.inviteMembers(invites);
const firstEmail = invites[0]!.email; const firstEmail = invites[0]!.email;

View File

@@ -13,19 +13,14 @@ export class TeamAccountsPageObject {
async setup(params = this.createTeamName()) { async setup(params = this.createTeamName()) {
await this.auth.signUpFlow('/home'); await this.auth.signUpFlow('/home');
await this.createTeam(params); await this.createTeam(params);
} }
getTeamFromSelector(teamSlug: string) { getTeamFromSelector(teamName: string) {
return this.page.locator( return this.page.locator(`[data-test="account-selector-team"]`, {
`[data-test="account-selector-team"][data-value="${teamSlug}"]`, hasText: teamName,
); });
}
selectAccount(teamName: string) {
return this.page.click(
`[data-test="account-selector-team"][data-name="${teamName}"]`,
);
} }
getTeams() { getTeams() {
@@ -48,8 +43,14 @@ export class TeamAccountsPageObject {
.click(); .click();
} }
openAccountsSelector() { async openAccountsSelector() {
return this.page.click('[data-test="account-selector-trigger"]'); 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()) { async createTeam({ teamName, slug } = this.createTeamName()) {
@@ -62,29 +63,41 @@ export class TeamAccountsPageObject {
await this.page.waitForURL(`/home/${slug}`); await this.page.waitForURL(`/home/${slug}`);
} }
async updateName(name: string) { async updateName(name: string, slug: string) {
await this.page.fill( await expect(async () => {
'[data-test="update-team-account-name-form"] input', await this.page.fill(
name, '[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) { 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 expect(
await this.page this.page.locator('[data-test="delete-team-form-confirm-input"]'),
.locator('[data-test="delete-team-form-confirm-input"]') ).toBeVisible();
.isVisible(),
).toBeTruthy();
await this.page.fill( await this.page.fill(
'[data-test="delete-team-form-confirm-input"]', '[data-test="delete-team-form-confirm-input"]',
teamName, teamName,
); );
await this.page.click('[data-test="delete-team-form-confirm-button"]');
await this.page.click('[data-test="delete-team-form-confirm-button"]');
await this.page.waitForURL('http://localhost:3000/home');
}).toPass();
} }
createTeamName() { createTeamName() {

View File

@@ -17,14 +17,14 @@ test.describe('Team Accounts', () => {
const { teamName, slug } = teamAccounts.createTeamName(); const { teamName, slug } = teamAccounts.createTeamName();
await teamAccounts.goToSettings(); await teamAccounts.goToSettings();
await teamAccounts.updateName(teamName); await teamAccounts.updateName(teamName, slug);
// the slug should be updated to match the new team name // the slug should be updated to match the new team name
await page.waitForURL(`http://localhost:3000/home/${slug}/settings`); await page.waitForURL(`http://localhost:3000/home/${slug}/settings`);
await teamAccounts.openAccountsSelector(); 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 teamAccounts.openAccountsSelector();
await expect( await expect(
teamAccounts.getTeamFromSelector(params.slug), teamAccounts.getTeamFromSelector(params.teamName),
).not.toBeVisible(); ).not.toBeVisible();
}); });
}); });

View File

@@ -9,21 +9,21 @@ test.describe('Team Billing', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
page = await browser.newPage(); page = await browser.newPage();
po = new TeamBillingPageObject(page); po = new TeamBillingPageObject(page);
await po.setup();
}); });
test('a team can subscribe to a plan', async () => { test('a team can subscribe to a plan', async () => {
await po.setup();
await po.teamAccounts.goToBilling(); await po.teamAccounts.goToBilling();
await po.billing.selectPlan(0); await po.billing.selectPlan(0);
await po.billing.proceedToCheckout(); await po.billing.proceedToCheckout();
await po.billing.stripe.waitForForm();
await po.billing.stripe.fillForm(); await po.billing.stripe.fillForm();
await po.billing.stripe.submitForm(); await po.billing.stripe.submitForm();
await expect(po.billing.successStatus()).toBeVisible({ await expect(po.billing.successStatus()).toBeVisible({
timeout: 30000, timeout: 25_000,
}); });
await po.billing.returnToBilling(); await po.billing.returnToBilling();

View File

@@ -17,11 +17,12 @@ test.describe('User Billing', () => {
await po.billing.selectPlan(0); await po.billing.selectPlan(0);
await po.billing.proceedToCheckout(); await po.billing.proceedToCheckout();
await po.billing.stripe.waitForForm();
await po.billing.stripe.fillForm(); await po.billing.stripe.fillForm();
await po.billing.stripe.submitForm(); await po.billing.stripe.submitForm();
await expect(po.billing.successStatus()).toBeVisible({ await expect(po.billing.successStatus()).toBeVisible({
timeout: 30000, timeout: 25_000,
}); });
await po.billing.returnToBilling(); await po.billing.returnToBilling();

View File

@@ -1,4 +1,4 @@
import { Page } from '@playwright/test'; import { Page, expect } from '@playwright/test';
import { StripePageObject } from './stripe.po'; import { StripePageObject } from './stripe.po';
@@ -13,10 +13,17 @@ export class BillingPageObject {
return this.page.locator('[data-test-plan]'); return this.page.locator('[data-test-plan]');
} }
selectPlan(index: number = 0) { async selectPlan(index = 0) {
const plans = this.plans(); 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() { manageBillingButton() {

View File

@@ -11,6 +11,12 @@ export class StripePageObject {
return this.page.frameLocator('[name="embedded-checkout"]'); return this.page.frameLocator('[name="embedded-checkout"]');
} }
async waitForForm() {
return expect(async () => {
await expect(this.billingCountry()).toBeVisible();
}).toPass();
}
async fillForm(params: { async fillForm(params: {
billingName?: string; billingName?: string;
cardNumber?: string; cardNumber?: string;
@@ -18,10 +24,6 @@ export class StripePageObject {
cvc?: string; cvc?: string;
billingCountry?: string; billingCountry?: string;
} = {}) { } = {}) {
expect(() => {
return this.getStripeCheckoutIframe().locator('form').isVisible();
});
const billingName = this.billingName(); const billingName = this.billingName();
const cardNumber = this.cardNumber(); const cardNumber = this.cardNumber();
const expiry = this.expiry(); const expiry = this.expiry();
@@ -55,10 +57,6 @@ export class StripePageObject {
return this.getStripeCheckoutIframe().locator('#billingName'); return this.getStripeCheckoutIframe().locator('#billingName');
} }
cardForm() {
return this.getStripeCheckoutIframe().locator('form');
}
billingCountry() { billingCountry() {
return this.getStripeCheckoutIframe().locator('#billingCountry'); return this.getStripeCheckoutIframe().locator('#billingCountry');
} }

View File

@@ -59,9 +59,9 @@ export function PlanPicker(
resolver: zodResolver( resolver: zodResolver(
z z
.object({ .object({
planId: z.string().min(1), planId: z.string(),
productId: z.string().min(1), productId: z.string(),
interval: z.string().min(1), interval: z.string(),
}) })
.refine( .refine(
(data) => { (data) => {

View File

@@ -150,7 +150,11 @@ export function AccountSelector({
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0" collisionPadding={20}> <PopoverContent
data-test={'account-selector-content'}
className="w-full p-0"
collisionPadding={20}
>
<Command> <Command>
<CommandInput placeholder={t('searchAccount')} className="h-9" /> <CommandInput placeholder={t('searchAccount')} className="h-9" />