Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-cats-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/testing': patch
---

Add `checkout` and `pricingTable` page objects.
66 changes: 43 additions & 23 deletions integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,16 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
const u = createTestUtils({ app, page, context });
await u.po.page.goToRelative('/pricing-table');

await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' });

await u.po.pricingTable.waitForMounted();
await expect(u.po.page.getByRole('heading', { name: 'Pro' })).toBeVisible();
});

test('when signed out, clicking get started button navigates to sign in page', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.page.goToRelative('/pricing-table');
await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' });

await u.po.page.getByText('Get started').first().click();
await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'plus' });
await u.po.signIn.waitForMounted();
await expect(u.po.page.getByText('Checkout')).toBeHidden();
});
Expand All @@ -44,43 +43,62 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table');
await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' });
await u.po.page.getByText('Get started').first().click();
await u.po.page.locator('.cl-checkout-root').waitFor({ state: 'attached' });
await expect(u.po.page.getByText(/Checkout/i)).toBeVisible();

await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'plus' });
await u.po.checkout.waitForMounted();
await expect(u.po.page.getByText('Checkout')).toBeVisible();
});

test('can subscribe to a plan', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table');
await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' });
// We have two plans, so subscribe to the first one
await u.po.page.getByText('Get started').first().click();

await u.po.page.locator('.cl-checkout-root').waitFor({ state: 'attached' });
await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'plus' });
await u.po.checkout.waitForMounted();
await u.po.checkout.fillTestCard();
await u.po.checkout.clickPayOrSubscribe();
await expect(u.po.page.getByText('Payment was successful!')).toBeVisible();
});

// Stripe uses multiple iframes, so we need to find the correct one
const frame = u.po.page.frameLocator('iframe[src*="elements-inner-payment"]');
await frame.getByLabel('Card number').fill('4242424242424242');
await frame.getByLabel('Expiration date').fill('1234');
await frame.getByLabel('Security code').fill('123');
await frame.getByLabel('ZIP code').fill('12345');
test('can upgrade to a new plan with saved card', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table');

await u.po.page.getByRole('button', { name: 'Pay $' }).click();
await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'pro', shouldSwitch: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a super strong opinion, but shouldSwitch is not super obvious. switchPlans maybe? Again not super strong opinion here.

await u.po.checkout.waitForMounted();
await u.po.checkout.clickPayOrSubscribe();
await expect(u.po.page.getByText('Payment was successful!')).toBeVisible();
});

test('can manage and cancel subscription', async ({ page, context }) => {
test('can downgrade to previous plan', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table');
await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' });

await u.po.page.getByText('Manage subscription').click();
await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'plus', shouldSwitch: true });
await u.po.checkout.waitForMounted();

await u.po.checkout.clickPayOrSubscribe();
await expect(u.po.page.getByText('Success!')).toBeVisible();
});

test('can manage and cancel subscription', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table');

await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.clickManageSubscription();
await u.po.page.getByRole('button', { name: 'Cancel subscription' }).click();
await u.po.page.getByRole('alertdialog').getByRole('button', { name: 'Cancel subscription' }).click();
await expect(u.po.page.getByRole('button', { name: 'Re-subscribe' }).first()).toBeVisible();
Expand All @@ -92,9 +110,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/user');

await u.po.userProfile.waitForMounted();
await u.po.page.getByText(/Billing/i).click();
await u.po.userProfile.switchToBillingTab();
await u.po.page.getByRole('button', { name: 'View all plans' }).click();
await u.po.pricingTable.waitForMounted();
await expect(u.po.page.getByRole('heading', { name: 'Pro' })).toBeVisible();
});
});
Expand Down
39 changes: 39 additions & 0 deletions packages/testing/src/playwright/unstable/page-objects/checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { EnhancedPage } from './app';
import { common } from './common';

export const createCheckoutPageObject = (testArgs: { page: EnhancedPage }) => {
const { page } = testArgs;
const self = {
...common(testArgs),
waitForMounted: (selector = '.cl-checkout-root') => {
return page.waitForSelector(selector, { state: 'attached' });
},
fillTestCard: async () => {
await self.fillCard({
number: '4242424242424242',
expiration: '1234',
cvc: '123',
country: 'United States',
zip: '12345',
});
},
fillCard: async (card: { number: string; expiration: string; cvc: string; country: string; zip: string }) => {
const frame = page.frameLocator('iframe[src*="elements-inner-payment"]');
await frame.getByLabel('Card number').fill(card.number);
await frame.getByLabel('Expiration date').fill(card.expiration);
await frame.getByLabel('Security code').fill(card.cvc);
await frame.getByLabel('Country').selectOption(card.country);
await frame.getByLabel('ZIP code').fill(card.zip);
},
clickPayOrSubscribe: async () => {
await page.getByRole('button', { name: /subscribe|pay\s/i }).click();
},
clickAddPaymentMethod: async () => {
await page.getByRole('radio', { name: 'Add payment method' }).click();
},
clickPaymentMethods: async () => {
await page.getByRole('radio', { name: 'Payment Methods' }).click();
},
};
return self;
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Page } from '@playwright/test';

import { createAppPageObject } from './app';
import { createCheckoutPageObject } from './checkout';
import { createClerkPageObject } from './clerk';
import { createExpectPageObject } from './expect';
import { createImpersonationPageObject } from './impersonation';
import { createKeylessPopoverPageObject } from './keylessPopover';
import { createOrganizationSwitcherComponentPageObject } from './organizationSwitcher';
import { createPricingTablePageObject } from './pricingTable';
import { createSessionTaskComponentPageObject } from './sessionTask';
import { createSignInComponentPageObject } from './signIn';
import { createSignUpComponentPageObject } from './signUp';
Expand All @@ -30,10 +32,12 @@ export const createPageObjects = ({
return {
page: app,
clerk: createClerkPageObject(testArgs),
checkout: createCheckoutPageObject(testArgs),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dstaley since we only expose checkout as an internal component currently, shall we follow the pattern here as well ? We can drop the internal prefix later once, checkout is a standalone component.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think given that Checkout is intended to be a public component in the near future we're fine to leave this as just checkout. We're under an unstable import so I don't think we really need to double up on the warnings.

expect: createExpectPageObject(testArgs),
impersonation: createImpersonationPageObject(testArgs),
keylessPopover: createKeylessPopoverPageObject(testArgs),
organizationSwitcher: createOrganizationSwitcherComponentPageObject(testArgs),
pricingTable: createPricingTablePageObject(testArgs),
sessionTask: createSessionTaskComponentPageObject(testArgs),
signIn: createSignInComponentPageObject(testArgs),
signUp: createSignUpComponentPageObject(testArgs),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { EnhancedPage } from './app';
import { common } from './common';

export const createPricingTablePageObject = (testArgs: { page: EnhancedPage }) => {
const { page } = testArgs;
const self = {
...common(testArgs),
waitForMounted: (selector = '.cl-pricingTable-root') => {
return page.waitForSelector(selector, { state: 'attached' });
},
clickManageSubscription: async () => {
await page.getByText('Manage subscription').click();
},
clickResubscribe: async () => {
await page.getByText('Re-subscribe').click();
},
startCheckout: async ({ planSlug, shouldSwitch }: { planSlug: string; shouldSwitch?: boolean }) => {
const targetButtonName =
shouldSwitch === true ? 'Switch to this plan' : shouldSwitch === false ? 'Get started' : /get|switch/i;
await page.locator(`.cl-pricingTableCard__${planSlug}`).getByRole('button', { name: targetButtonName }).click();
},
};
return self;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const createUserProfileComponentPageObject = (testArgs: { page: EnhancedP
switchToSecurityTab: async () => {
await page.getByText(/Security/i).click();
},
switchToBillingTab: async () => {
await page.getByText(/Billing/i).click();
},
waitForMounted: () => {
return page.waitForSelector('.cl-userProfile-root', { state: 'attached' });
},
Expand Down