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';
|
import { AuthPageObject } from '../authentication/auth.po';
|
||||||
|
|
||||||
export class AccountPageObject {
|
export class AccountPageObject {
|
||||||
@@ -20,21 +21,57 @@ 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() {
|
||||||
|
|||||||
@@ -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,8 +52,10 @@ 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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user