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';
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() {

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';
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

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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();
});
});

View File

@@ -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();

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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');
}

View File

@@ -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) => {

View File

@@ -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" />