Added sample Portal test to PlayWright suite

refs: https://github.com/TryGhost/Toolbox/issues/479
This commit is contained in:
Sam Lord 2022-11-24 17:46:58 +00:00
parent 0bdccb6497
commit 8b80233ae6
5 changed files with 126 additions and 52 deletions

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
cache: yarn
- name: Install dependencies

View File

@ -1,13 +1,17 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
timeout: 60 * 1000,
workers: 1,
use: {
// Use a single browser since we can't simultaneously test multiple browsers
browserName: 'chromium',
baseURL: 'http://localhost:2368'
},
webServer: {
baseURL: process.env.TEST_URL ?? 'http://localhost:2368'
}
};
if (!process.env.TEST_URL) {
config.webServer = {
command: 'yarn start',
env: {
// TODO: Use `testing` when starting a server
@ -15,7 +19,7 @@ const config = {
},
reuseExistingServer: !process.env.CI,
url: 'http://localhost:2368'
}
};
};
}
module.exports = config;

View File

@ -1,6 +1,5 @@
const {expect, test} = require('@playwright/test');
const {setupGhost, setupStripe, createTier} = require('./utils');
const ObjectID = require('bson-objectid').default;
const {setupGhost, setupStripe, createTier, createOffer} = require('./utils');
test.describe('Ghost Admin', () => {
test.beforeEach(async ({page}) => {
@ -36,44 +35,12 @@ test.describe('Ghost Admin', () => {
monthlyPrice: 5,
yearlyPrice: 50
});
await page.goto('/ghost');
await page.locator('[data-test-nav="offers"]').click();
// Keep offer names unique
let offerName = `Get 5% Off! (${new ObjectID()})`;
if (await page.locator('.gh-offers-list').isVisible()) {
const listItem = page.locator('[data-test-list="offers-list-item"]').first();
while (await page.locator('[data-test-list="offers-list-item"]').first().isVisible().then(() => true).catch(() => false)) {
await listItem.getByRole('link', {name: 'arrow-right'}).click();
await page.getByRole('button', {name: 'Archive offer'}).click();
await page
.locator('.modal-content')
.filter({hasText: 'Archive offer'})
.first()
.getByRole('button', {name: 'Archive'})
.click();
const statusDropdown = await page.getByRole('button', {name: 'Archived offers arrow-down-small'});
if (await statusDropdown.isVisible()) {
await statusDropdown.click();
await page.getByRole('option', {name: 'Active offers'}).click();
}
}
}
await page.getByRole('link', {name: 'New offer'}).click();
await page.locator('[data-test-input="offer-name"]').fill(offerName);
await page.locator('input#amount').fill('5');
const priceId = await page.locator(`.gh-select-product-cadence>select>option`).getByText(`${tierName} - Monthly`).getAttribute('value');
await page.locator('.gh-select-product-cadence>select').selectOption(priceId);
await page.getByRole('button', {name: 'Save'}).click();
await page.locator('[data-test-button="save"] [data-test-task-button-state="success"]').waitFor({
state: 'visible',
timeout: 1000
const offerName = await createOffer(page, {
name: 'Get 5% Off!',
tierName,
percentOff: 5
});
// Click the "offers" link to go back
await page.locator('[data-test-nav="offers"]').click();
await page.locator('.gh-offers-list').waitFor({state: 'visible', timeout: 1000});
await expect(page.locator('.gh-offers-list')).toContainText(tierName);

View File

@ -1,5 +1,5 @@
const {expect, test} = require('@playwright/test');
const {setupGhost} = require('./utils');
const {setupGhost, setupStripe, createTier, createOffer, completeStripeSubscription} = require('./utils');
test.describe('Ghost Frontend', () => {
test.beforeEach(async ({page}) => {
@ -14,11 +14,37 @@ test.describe('Ghost Frontend', () => {
});
test.describe('Portal flows', () => {
test('Loads the homepage', async ({page}) => {
const response = await page.goto('/');
expect(response.status()).toEqual(200);
test('Uses an offer successfully', async ({page}) => {
await setupStripe(page);
await createTier(page, {
name: 'Portal Tier',
monthlyPrice: 6,
yearlyPrice: 60
});
const offerName = await createOffer(page, {
name: 'Black Friday Special',
tierName: 'Portal Tier',
percentOff: 10
});
// TODO: Implement a real portal test
// TODO: Click on the offer, copy the link, goto the link
await page.locator('[data-test-list="offer-name"]').filter({hasText: offerName}).click();
const portalUrl = await page.locator('input#url').inputValue();
await page.goto(portalUrl);
const portalFrame = page.frameLocator('#ghost-portal-root div iframe');
await portalFrame.locator('#input-name').fill('Testy McTesterson');
await portalFrame.locator('#input-email').fill('testy@example.com');
await portalFrame.getByRole('button', {name: 'Continue'}).click();
await completeStripeSubscription(page);
// Wait for success notification to say we have subscribed successfully
const gotNotification = await page.frameLocator('iframe >> nth=1').getByText('Success! Check your email for magic link').waitFor({
state: 'visible',
timeout: 10000
}).then(() => true).catch(() => false);
test.expect(gotNotification, 'Did not get portal success notification').toBeTruthy();
});
});
});

View File

@ -1,4 +1,5 @@
const DataGenerator = require('../../utils/fixtures/data-generator');
const ObjectID = require('bson-objectid').default;
/**
* Setup Ghost Admin, or login if there's a login prompt
@ -28,10 +29,14 @@ const setupGhost = async (page) => {
page.locator('.gh-nav').waitFor(options).then(() => actions.noAction).catch(() => {})
]);
// TODO: Use env variables in CI
// Add owner user data from usual fixture
const ownerUser = DataGenerator.Content.users.find(user => user.id === '1');
if (process.env.CI && process.env.TEST_URL) {
ownerUser.email = process.env.TEST_OWNER_EMAIL;
ownerUser.password = process.env.TEST_OWNER_PASSWORD;
}
if (action === actions.signin) {
// Fill email + password
await page.locator('#identification').fill(ownerUser.email);
@ -55,6 +60,7 @@ const setupGhost = async (page) => {
}
};
// Only ever setup Stripe once, for performance reasons
let isStripeSetup = false;
/**
@ -130,8 +136,79 @@ const createTier = async (page, {name, monthlyPrice, yearlyPrice}) => {
await page.waitForSelector('input[data-test-input="tier-name"]', {state: 'detached'});
};
/**
* Create an offer on a tier
* @param {import('@playwright/test').Page} page
* @param {object} options
* @param {string} options.name
* @param {string} options.tierName
* @param {number} options.percentOff
* @returns {Promise<string>} Unique offer name
*/
const createOffer = async (page, {name, tierName, percentOff}) => {
await page.goto('/ghost');
await page.locator('[data-test-nav="offers"]').click();
// Keep offer names unique & <= 40 characters
let offerName = `${name} (${new ObjectID().toHexString().slice(0, 40 - name.length - 3)})`;
// Archive other offers to keep the list tidy
// We only need 1 offer to be active at a time
// Either the list of active offers loads, or the CTA when no offers exist
while (await Promise.race([
page.locator('.gh-offers-list .gh-list-header').filter({hasText: 'active'}).waitFor({state: 'visible', timeout: 1000}).then(() => true).catch(() => false),
page.locator('.gh-offers-list-cta').waitFor({state: 'visible', timeout: 1000}).then(() => false).catch(() => false)
])) {
const listItem = page.locator('[data-test-list="offers-list-item"]').first();
await listItem.getByRole('link', {name: 'arrow-right'}).click();
await page.getByRole('button', {name: 'Archive offer'}).click();
await page
.locator('.modal-content')
.filter({hasText: 'Archive offer'})
.first()
.getByRole('button', {name: 'Archive'})
.click();
// TODO: Use a more resilient selector
const statusDropdown = await page.getByRole('button', {name: 'Archived offers arrow-down-small'});
await statusDropdown.waitFor({
state: 'visible',
timeout: 1000
});
await statusDropdown.click();
await page.getByRole('option', {name: 'Active offers'}).click();
}
await page.getByRole('link', {name: 'New offer'}).click();
await page.locator('[data-test-input="offer-name"]').fill(offerName);
await page.locator('input#amount').fill(`${percentOff}`);
const priceId = await page.locator(`.gh-select-product-cadence>select>option`).getByText(`${tierName} - Monthly`).getAttribute('value');
await page.locator('.gh-select-product-cadence>select').selectOption(priceId);
await page.getByRole('button', {name: 'Save'}).click();
// Wait for the "Saved" button, ensures that next clicks don't trigger the unsaved work modal
await page.locator('[data-test-button="save"] [data-test-task-button-state="success"]').waitFor({
state: 'visible',
timeout: 1000
});
await page.locator('[data-test-nav="offers"]').click();
return offerName;
};
const completeStripeSubscription = async (page) => {
await page.locator('#cardNumber').fill('4242 4242 4242 4242');
await page.locator('#cardExpiry').fill('04 / 24');
await page.locator('#cardCvc').fill('424');
await page.locator('#billingName').fill('Testy McTesterson');
await page.getByRole('combobox', {name: 'Country or region'}).selectOption('US');
await page.locator('#billingPostalCode').fill('42424');
await page.getByTestId('hosted-payment-submit-button').click();
};
module.exports = {
setupGhost,
setupStripe,
createTier
createTier,
createOffer,
completeStripeSubscription
};