Skip to content

Commit fd06558

Browse files
authored
Migrated Portal signup link tests (#26888)
no ref
1 parent e8a9464 commit fd06558

17 files changed

Lines changed: 315 additions & 105 deletions

File tree

e2e/helpers/pages/admin/settings/sections/portal-section.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {BasePage} from '@/helpers/pages';
22
import {Locator, Page} from '@playwright/test';
33

4+
type PaidSignupCadence = 'monthly' | 'yearly';
5+
46
export class PortalSection extends BasePage {
57
readonly section: Locator;
68
readonly customizeButton: Locator;
79
readonly portalModal: Locator;
810
readonly linksTab: Locator;
11+
readonly linksTierSelectControl: Locator;
912
readonly freeTierToggleLabel: Locator;
1013

1114
constructor(page: Page) {
@@ -15,6 +18,7 @@ export class PortalSection extends BasePage {
1518
this.customizeButton = this.section.getByRole('button', {name: 'Customize'});
1619
this.portalModal = page.getByTestId('portal-modal');
1720
this.linksTab = this.portalModal.getByRole('tab', {name: 'Links'});
21+
this.linksTierSelectControl = this.portalModal.locator('span:has-text("Tier:") + div').first();
1822
this.freeTierToggleLabel = this.portalModal.locator('label').filter({hasText: 'Free'}).first();
1923
}
2024

@@ -31,6 +35,38 @@ export class PortalSection extends BasePage {
3135
await this.linksTab.click();
3236
}
3337

38+
async selectLinksTier(name: string): Promise<void> {
39+
await this.openLinksTab();
40+
await this.linksTierSelectControl.scrollIntoViewIfNeeded();
41+
await this.linksTierSelectControl.click();
42+
await this.page.getByRole('option', {name, exact: true}).click();
43+
}
44+
45+
async getPaidSignupLinkForTier(name: string, tierId: string, cadence: PaidSignupCadence): Promise<string> {
46+
await this.selectLinksTier(name);
47+
48+
const label = cadence === 'monthly' ? 'Signup / Monthly' : 'Signup / Yearly';
49+
const linkInput = this.portalModal.getByLabel(label);
50+
await linkInput.waitFor({state: 'visible'});
51+
const inputId = await linkInput.getAttribute('id');
52+
53+
if (!inputId) {
54+
throw new Error(`Portal ${cadence} signup link input was not found`);
55+
}
56+
57+
await this.page.waitForFunction(({expectedTierId, targetInputId}: {expectedTierId: string; targetInputId: string}) => {
58+
const element = document.getElementById(targetInputId);
59+
return element instanceof HTMLInputElement && element.value.includes(expectedTierId);
60+
}, {
61+
expectedTierId: tierId,
62+
targetInputId: inputId
63+
}, {
64+
timeout: 5000
65+
});
66+
67+
return await linkInput.inputValue();
68+
}
69+
3470
async getLinkValue(label: string): Promise<string> {
3571
await this.openLinksTab();
3672
const linkInput = this.portalModal.getByLabel(label);

e2e/helpers/pages/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './base-page';
22
export * from './admin';
33
export * from './portal';
44
export * from './public';
5-
5+
export * from './stripe';

e2e/helpers/pages/portal/sign-up-page.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export class SignUpPage extends PortalPage {
66
readonly nameInput: Locator;
77
readonly signupButton: Locator;
88
readonly signinLink: Locator;
9+
readonly paidTierCard: Locator;
10+
readonly paidTierSelectButton: Locator;
11+
readonly continueButton: Locator;
912

1013
constructor(page: Page) {
1114
super(page);
@@ -14,6 +17,9 @@ export class SignUpPage extends PortalPage {
1417
this.emailInput = this.portalFrame.getByRole('textbox', {name: 'Email'});
1518
this.signupButton = this.portalFrame.getByRole('button', {name: 'Sign up'});
1619
this.signinLink = this.portalFrame.getByRole('button', {name: 'Sign in'});
20+
this.paidTierCard = this.portalFrame.locator('[data-test-tier="paid"]').first();
21+
this.paidTierSelectButton = this.paidTierCard.locator('[data-test-button="select-tier"]');
22+
this.continueButton = this.portalFrame.getByRole('button', {name: 'Continue'});
1723
}
1824

1925
async fillAndSubmit(email: string, name?: string): Promise<void> {
@@ -23,4 +29,24 @@ export class SignUpPage extends PortalPage {
2329
await this.emailInput.fill(email);
2430
await this.signupButton.click();
2531
}
26-
}
32+
33+
async fillAndSubmitPaidSignup(email: string, name?: string): Promise<void> {
34+
if (name) {
35+
await this.nameInput.fill(name);
36+
}
37+
await this.emailInput.fill(email);
38+
await this.selectPaidTier();
39+
await this.continueIfVisible();
40+
}
41+
42+
async selectPaidTier(): Promise<void> {
43+
await this.paidTierCard.waitFor({state: 'visible'});
44+
await this.paidTierSelectButton.click();
45+
}
46+
47+
async continueIfVisible(): Promise<void> {
48+
if (await this.continueButton.isVisible()) {
49+
await this.continueButton.click();
50+
}
51+
}
52+
}

e2e/helpers/pages/public/home-page.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export class HomePage extends PublicPage {
2020
await this.portalRoot.waitFor({state: 'attached'});
2121
}
2222

23+
async openPortal(): Promise<void> {
24+
await this.openPortalViaSubscribeButton();
25+
}
26+
2327
async openAccountPortal(): Promise<void> {
28+
await this.accountButton.waitFor({state: 'visible'});
2429
await this.portal.clickLinkAndWaitForPopup(this.accountButton);
2530
}
2631

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {BasePage} from '@/helpers/pages';
2+
import {Locator, Page} from '@playwright/test';
3+
4+
export class FakeStripeCheckoutPage extends BasePage {
5+
readonly title: Locator;
6+
7+
constructor(page: Page) {
8+
super(page);
9+
10+
this.title = page.getByRole('heading', {name: 'Fake Stripe Checkout'});
11+
}
12+
13+
async waitUntilLoaded(): Promise<void> {
14+
await this.title.waitFor({state: 'visible'});
15+
}
16+
}

e2e/helpers/pages/stripe/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './fake-checkout-page';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './sign-in';
22
export * from './signup';
3+
export * from './tiers';

e2e/helpers/playwright/flows/signup.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import {FakeStripeCheckoutPage, HomePage} from '@/helpers/pages';
12
import {Page} from '@playwright/test';
2-
import {PublicPage} from '@/public-pages';
33
import {SignUpPage, SignUpSuccessPage} from '@/portal-pages';
44
import {faker} from '@faker-js/faker';
5+
import type {StripeTestService} from '@/helpers/services/stripe';
56

67
export async function signupViaPortal(page: Page): Promise<{emailAddress: string; name: string}> {
7-
const publicPage = new PublicPage(page);
8-
await publicPage.openPortalViaSubscribeButton();
8+
const homePage = new HomePage(page);
9+
await homePage.goto();
10+
await homePage.openPortal();
911

1012
const signUpPage = new SignUpPage(page);
1113
const emailAddress = `test${faker.string.uuid()}@ghost.org`;
@@ -18,3 +20,30 @@ export async function signupViaPortal(page: Page): Promise<{emailAddress: string
1820

1921
return {emailAddress, name};
2022
}
23+
24+
export async function completePaidSignupViaPortal(page: Page, stripe: StripeTestService, opts?: {emailAddress?: string; name?: string}): Promise<{emailAddress: string; name: string}> {
25+
const homePage = new HomePage(page);
26+
await homePage.goto();
27+
await homePage.openPortal();
28+
29+
const signUpPage = new SignUpPage(page);
30+
const emailAddress = opts?.emailAddress ?? `test${faker.string.uuid()}@ghost.org`;
31+
const name = opts?.name ?? faker.person.fullName();
32+
33+
await signUpPage.waitForPortalToOpen();
34+
await signUpPage.fillAndSubmitPaidSignup(emailAddress, name);
35+
36+
const fakeCheckoutPage = new FakeStripeCheckoutPage(page);
37+
await fakeCheckoutPage.waitUntilLoaded();
38+
await stripe.completeLatestSubscriptionCheckout({name});
39+
40+
const latestCheckoutSession = stripe.getCheckoutSessions().at(-1);
41+
const successUrl = latestCheckoutSession?.response.success_url;
42+
if (!successUrl) {
43+
throw new Error('Latest Stripe checkout session does not include a success URL');
44+
}
45+
46+
await page.goto(successUrl);
47+
48+
return {emailAddress, name};
49+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {SettingsService} from '@/helpers/services/settings/settings-service';
2+
import {TiersService} from '@/helpers/services/tiers/tiers-service';
3+
import type {AdminTier, TierCreateInput} from '@/helpers/services/tiers/tiers-service';
4+
import type {HttpClient} from '@/data-factory';
5+
6+
export async function createPaidPortalTier(
7+
request: HttpClient,
8+
input: Omit<TierCreateInput, 'visibility'> & Partial<Pick<TierCreateInput, 'visibility'>>
9+
): Promise<AdminTier> {
10+
const tiersService = new TiersService(request);
11+
const settingsService = new SettingsService(request);
12+
13+
const tier = await tiersService.createTier({
14+
...input,
15+
visibility: input.visibility ?? 'public'
16+
});
17+
18+
await settingsService.setPortalPlans(['free', 'monthly', 'yearly']);
19+
20+
return tier;
21+
}

e2e/helpers/services/settings/settings-service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {HttpClient as APIRequest} from '@/data-factory';
22

33
export interface Setting {
44
key: string;
5-
value: string | boolean | null;
5+
value: string | boolean | string[] | null;
66
}
77

88
export interface SettingsResponse {
@@ -11,6 +11,7 @@ export interface SettingsResponse {
1111

1212
export type CommentsEnabled = 'all' | 'paid' | 'off';
1313
export type MembersSignupAccess = 'all' | 'invite' | 'paid' | 'none';
14+
export type PortalPlan = 'free' | 'monthly' | 'yearly';
1415

1516
export class SettingsService {
1617
private readonly request: APIRequest;
@@ -58,6 +59,10 @@ export class SettingsService {
5859
return await this.updateSettings([{key: 'members_signup_access', value}]);
5960
}
6061

62+
async setPortalPlans(value: PortalPlan[]) {
63+
return await this.updateSettings([{key: 'portal_plans', value}]);
64+
}
65+
6166
/**
6267
* Set Stripe keys to simulate a connected Stripe account
6368
* Uses direct Stripe keys (not Connect) as they're not filtered by the API

0 commit comments

Comments
 (0)