mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 06:35:49 +03:00
Added sample Portal test to PlayWright suite
refs: https://github.com/TryGhost/Toolbox/issues/479
This commit is contained in:
parent
0bdccb6497
commit
8b80233ae6
2
.github/workflows/browser-tests.yml
vendored
2
.github/workflows/browser-tests.yml
vendored
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user