|
1 | 1 | import {OffersService} from '@/helpers/services/offers/offers-service'; |
2 | | -import {PublicPage} from '@/helpers/pages'; |
3 | | -import {createPaidPortalTier, expect, test} from '@/helpers/playwright'; |
| 2 | +import {PortalOfferPage, PublicPage} from '@/helpers/pages'; |
| 3 | +import {SettingsService} from '@/helpers/services/settings/settings-service'; |
| 4 | +import {createPaidPortalTier, expect, redeemOfferViaPortal, test} from '@/helpers/playwright'; |
| 5 | +import type {AdminOffer, OfferCreateInput} from '@/helpers/services/offers/offers-service'; |
| 6 | +import type {HttpClient} from '@/data-factory'; |
| 7 | +import type {StripeTestService} from '@/helpers/services/stripe'; |
| 8 | + |
| 9 | +const MEMBER_NAME = 'Testy McTesterson'; |
| 10 | + |
| 11 | +type SignupOfferInput = Pick<OfferCreateInput, 'amount' | 'duration' | 'duration_in_months' | 'type'> & { |
| 12 | + codePrefix: string; |
| 13 | + tierNamePrefix: string; |
| 14 | +}; |
| 15 | + |
| 16 | +// TODO: Move this setup into an OfferFactory-backed helper that owns tier creation, |
| 17 | +// portal settings, and Stripe sync instead of keeping it local to the test file. |
| 18 | +async function createSignupOffer(request: HttpClient, stripe: StripeTestService, input: SignupOfferInput): Promise<AdminOffer> { |
| 19 | + const offersService = new OffersService(request); |
| 20 | + const settingsService = new SettingsService(request); |
| 21 | + const suffix = Date.now(); |
| 22 | + const tierName = `${input.tierNamePrefix} ${suffix}`; |
| 23 | + |
| 24 | + await settingsService.updateSettings([{key: 'portal_button', value: true}]); |
| 25 | + |
| 26 | + const tier = await createPaidPortalTier(request, { |
| 27 | + name: tierName, |
| 28 | + currency: 'usd', |
| 29 | + monthly_price: 600, |
| 30 | + yearly_price: 6000 |
| 31 | + }); |
| 32 | + await waitForTierStripeSync(stripe, tierName); |
| 33 | + |
| 34 | + return await offersService.createOffer({ |
| 35 | + name: 'Black Friday Special', |
| 36 | + code: `${input.codePrefix}-${suffix}`, |
| 37 | + cadence: 'month', |
| 38 | + amount: input.amount, |
| 39 | + duration: input.duration, |
| 40 | + duration_in_months: input.duration_in_months ?? null, |
| 41 | + tierId: tier.id, |
| 42 | + type: input.type |
| 43 | + }); |
| 44 | +} |
| 45 | + |
| 46 | +async function waitForTierStripeSync(stripe: StripeTestService, tierName: string): Promise<void> { |
| 47 | + await expect.poll(() => { |
| 48 | + const product = stripe.getProducts().find(item => item.name === tierName); |
| 49 | + if (!product) { |
| 50 | + return 0; |
| 51 | + } |
| 52 | + |
| 53 | + return stripe.getPrices().filter(item => item.product === product.id).length; |
| 54 | + }, {timeout: 10000}).toBe(2); |
| 55 | +} |
4 | 56 |
|
5 | 57 | test.describe('Ghost Public - Portal Offers', () => { |
6 | 58 | test.use({stripeEnabled: true}); |
@@ -53,4 +105,112 @@ test.describe('Ghost Public - Portal Offers', () => { |
53 | 105 | await expect(publicPage.portalPopupFrame).toHaveCount(0); |
54 | 106 | await expect(page).not.toHaveURL(/#\/portal\/offers\//); |
55 | 107 | }); |
| 108 | + |
| 109 | + test('free trial offer opens in portal - redemption shows free trial state', async ({page, stripe}) => { |
| 110 | + const offer = await createSignupOffer(page.request, stripe!, { |
| 111 | + amount: 14, |
| 112 | + codePrefix: 'trial-offer', |
| 113 | + duration: 'trial', |
| 114 | + tierNamePrefix: 'Trial Offer Tier', |
| 115 | + type: 'trial' |
| 116 | + }); |
| 117 | + |
| 118 | + const publicPage = new PublicPage(page); |
| 119 | + await publicPage.gotoOfferCode(offer.code); |
| 120 | + |
| 121 | + const offerPage = new PortalOfferPage(page); |
| 122 | + await offerPage.waitForOfferPage(offer.name); |
| 123 | + await expect(offerPage.headingWithText(offer.name)).toBeVisible(); |
| 124 | + await expect(offerPage.text('14 days free')).toBeVisible(); |
| 125 | + await expect(offerPage.text('Try free for 14 days')).toBeVisible(); |
| 126 | + |
| 127 | + const {accountPage, subscription} = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); |
| 128 | + await expect(accountPage.freeTrialLabel).toBeVisible(); |
| 129 | + expect(subscription.offer?.id).toBe(offer.id); |
| 130 | + expect(subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); |
| 131 | + expect(subscription.status).toBe('trialing'); |
| 132 | + }); |
| 133 | + |
| 134 | + test('one-time discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { |
| 135 | + const offer = await createSignupOffer(page.request, stripe!, { |
| 136 | + amount: 10, |
| 137 | + codePrefix: 'once-offer', |
| 138 | + duration: 'once', |
| 139 | + tierNamePrefix: 'One-time Offer Tier', |
| 140 | + type: 'percent' |
| 141 | + }); |
| 142 | + |
| 143 | + const publicPage = new PublicPage(page); |
| 144 | + await publicPage.gotoOfferCode(offer.code); |
| 145 | + |
| 146 | + const offerPage = new PortalOfferPage(page); |
| 147 | + await offerPage.waitForOfferPage(offer.name); |
| 148 | + await expect(offerPage.headingWithText(offer.name)).toBeVisible(); |
| 149 | + await expect(offerPage.text(/^10% off$/)).toBeVisible(); |
| 150 | + await expect(offerPage.text(/\$5\.40/)).toBeVisible(); |
| 151 | + await expect(offerPage.text('10% off for first month')).toBeVisible(); |
| 152 | + |
| 153 | + const {accountPage, subscription} = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); |
| 154 | + await expect(accountPage.offerLabel).toContainText('$5.40/month'); |
| 155 | + await expect(accountPage.offerLabel).toContainText('Ends'); |
| 156 | + expect(subscription.offer?.id).toBe(offer.id); |
| 157 | + expect(subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); |
| 158 | + expect(subscription.offer?.duration).toBe('once'); |
| 159 | + }); |
| 160 | + |
| 161 | + test('repeating discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { |
| 162 | + const offer = await createSignupOffer(page.request, stripe!, { |
| 163 | + amount: 10, |
| 164 | + codePrefix: 'repeating-offer', |
| 165 | + duration: 'repeating', |
| 166 | + duration_in_months: 3, |
| 167 | + tierNamePrefix: 'Repeating Offer Tier', |
| 168 | + type: 'percent' |
| 169 | + }); |
| 170 | + |
| 171 | + const publicPage = new PublicPage(page); |
| 172 | + await publicPage.gotoOfferCode(offer.code); |
| 173 | + |
| 174 | + const offerPage = new PortalOfferPage(page); |
| 175 | + await offerPage.waitForOfferPage(offer.name); |
| 176 | + await expect(offerPage.headingWithText(offer.name)).toBeVisible(); |
| 177 | + await expect(offerPage.text(/^10% off$/)).toBeVisible(); |
| 178 | + await expect(offerPage.text(/\$5\.40/)).toBeVisible(); |
| 179 | + await expect(offerPage.text('10% off for first 3 months')).toBeVisible(); |
| 180 | + |
| 181 | + const {accountPage, subscription} = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); |
| 182 | + await expect(accountPage.offerLabel).toContainText('$5.40/month'); |
| 183 | + await expect(accountPage.offerLabel).toContainText('Ends'); |
| 184 | + expect(subscription.offer?.id).toBe(offer.id); |
| 185 | + expect(subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); |
| 186 | + expect(subscription.offer?.duration).toBe('repeating'); |
| 187 | + expect(subscription.offer?.duration_in_months).toBe(3); |
| 188 | + }); |
| 189 | + |
| 190 | + test('forever discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { |
| 191 | + const offer = await createSignupOffer(page.request, stripe!, { |
| 192 | + amount: 10, |
| 193 | + codePrefix: 'forever-offer', |
| 194 | + duration: 'forever', |
| 195 | + tierNamePrefix: 'Forever Offer Tier', |
| 196 | + type: 'percent' |
| 197 | + }); |
| 198 | + |
| 199 | + const publicPage = new PublicPage(page); |
| 200 | + await publicPage.gotoOfferCode(offer.code); |
| 201 | + |
| 202 | + const offerPage = new PortalOfferPage(page); |
| 203 | + await offerPage.waitForOfferPage(offer.name); |
| 204 | + await expect(offerPage.headingWithText(offer.name)).toBeVisible(); |
| 205 | + await expect(offerPage.text(/^10% off$/)).toBeVisible(); |
| 206 | + await expect(offerPage.text(/\$5\.40/)).toBeVisible(); |
| 207 | + await expect(offerPage.text('10% off forever')).toBeVisible(); |
| 208 | + |
| 209 | + const {accountPage, subscription} = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); |
| 210 | + await expect(accountPage.offerLabel).toContainText('$5.40/month'); |
| 211 | + await expect(accountPage.offerLabel).toContainText('Forever'); |
| 212 | + expect(subscription.offer?.id).toBe(offer.id); |
| 213 | + expect(subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); |
| 214 | + expect(subscription.offer?.duration).toBe('forever'); |
| 215 | + }); |
56 | 216 | }); |
0 commit comments