2022-11-24 18:11:40 +03:00
|
|
|
const DataGenerator = require('../../utils/fixtures/data-generator');
|
2022-11-24 20:46:58 +03:00
|
|
|
const ObjectID = require('bson-objectid').default;
|
2022-11-24 18:11:40 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Setup Ghost Admin, or login if there's a login prompt
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
*/
|
|
|
|
const setupGhost = async (page) => {
|
|
|
|
await page.goto('/ghost');
|
|
|
|
|
|
|
|
const actions = {
|
|
|
|
signin: Symbol(),
|
|
|
|
setup: Symbol(),
|
|
|
|
noAction: Symbol()
|
|
|
|
};
|
|
|
|
/**
|
|
|
|
* Using this to fix ESLint
|
|
|
|
* @type {object}
|
|
|
|
* @field {string} state
|
|
|
|
* @field {number} timeout
|
|
|
|
*/
|
|
|
|
const options = {
|
|
|
|
state: 'visible',
|
|
|
|
timeout: 10000
|
|
|
|
};
|
|
|
|
const action = await Promise.race([
|
|
|
|
page.locator('.gh-signin').waitFor(options).then(() => actions.signin).catch(() => {}),
|
|
|
|
page.locator('.gh-setup').waitFor(options).then(() => actions.setup).catch(() => {}),
|
|
|
|
page.locator('.gh-nav').waitFor(options).then(() => actions.noAction).catch(() => {})
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Add owner user data from usual fixture
|
|
|
|
const ownerUser = DataGenerator.Content.users.find(user => user.id === '1');
|
|
|
|
|
2022-11-24 20:46:58 +03:00
|
|
|
if (process.env.CI && process.env.TEST_URL) {
|
|
|
|
ownerUser.email = process.env.TEST_OWNER_EMAIL;
|
|
|
|
ownerUser.password = process.env.TEST_OWNER_PASSWORD;
|
|
|
|
}
|
|
|
|
|
2022-11-24 18:11:40 +03:00
|
|
|
if (action === actions.signin) {
|
|
|
|
// Fill email + password
|
|
|
|
await page.locator('#identification').fill(ownerUser.email);
|
|
|
|
await page.locator('#password').fill(ownerUser.password);
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.getByRole('button', {name: 'Sign in'}).click();
|
2022-11-24 18:11:40 +03:00
|
|
|
// Confirm we have reached Ghost Admin
|
|
|
|
await page.locator('.gh-nav').waitFor(options);
|
|
|
|
} else if (action === actions.setup) {
|
|
|
|
// Complete setup process
|
|
|
|
await page.getByPlaceholder('The Daily Awesome').click();
|
|
|
|
await page.getByPlaceholder('The Daily Awesome').fill('The Local Test');
|
|
|
|
|
|
|
|
// Add owner user data from usual fixture
|
|
|
|
await page.getByPlaceholder('Jamie Larson').fill(ownerUser.name);
|
|
|
|
await page.getByPlaceholder('jamie@example.com').fill(ownerUser.email);
|
|
|
|
await page.getByPlaceholder('At least 10 characters').fill(ownerUser.password);
|
|
|
|
|
|
|
|
await page.getByPlaceholder('At least 10 characters').press('Enter');
|
|
|
|
await page.locator('.gh-done-pink').click();
|
|
|
|
await page.locator('.gh-nav').waitFor(options);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-12-01 15:05:20 +03:00
|
|
|
const setupStripe = async (page, stripConnectIntegrationToken) => {
|
|
|
|
await deleteAllMembers(page);
|
|
|
|
await page.locator('.gh-nav a[href="#/settings/"]').click();
|
|
|
|
await page.locator('.gh-setting-group').filter({hasText: 'Membership'}).click();
|
|
|
|
if (await page.isVisible('.gh-btn-stripe-status.connected')) {
|
|
|
|
// Disconnect if already connected
|
|
|
|
await page.locator('.gh-btn-stripe-status.connected').click();
|
|
|
|
await page.locator('.modal-content .gh-btn-stripe-disconnect').first().click();
|
|
|
|
await page
|
|
|
|
.locator('.modal-content')
|
|
|
|
.filter({hasText: 'Are you sure you want to disconnect?'})
|
|
|
|
.first()
|
|
|
|
.getByRole('button', {name: 'Disconnect'})
|
|
|
|
.click();
|
|
|
|
} else {
|
|
|
|
await page.locator('.gh-setting-members-tierscontainer .stripe-connect').click();
|
|
|
|
}
|
|
|
|
await page.getByPlaceholder('Paste your secure key here').first().fill(stripConnectIntegrationToken);
|
|
|
|
await page.getByRole('button', {name: 'Save Stripe settings'}).click();
|
|
|
|
await page.getByRole('button', {name: 'OK'}).click();
|
|
|
|
};
|
|
|
|
|
2022-11-24 18:11:40 +03:00
|
|
|
/**
|
2022-12-01 03:34:19 +03:00
|
|
|
* Delete all members, 1 by 1, using the UI
|
2022-11-24 18:11:40 +03:00
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
*/
|
2022-12-01 03:34:19 +03:00
|
|
|
const deleteAllMembers = async (page) => {
|
|
|
|
await page.locator('a[href="#/members/"]').first().click();
|
|
|
|
|
|
|
|
const firstMember = page.locator('.gh-list tbody tr').first();
|
|
|
|
while (await Promise.race([
|
|
|
|
firstMember.waitFor({state: 'visible', timeout: 1000}).then(() => true),
|
|
|
|
page.locator('.gh-members-empty').waitFor({state: 'visible', timeout: 1000}).then(() => false)
|
|
|
|
]).catch(() => false)) {
|
|
|
|
await firstMember.click();
|
|
|
|
await page.locator('.view-actions .dropdown > button').click();
|
|
|
|
await page.getByRole('button', {name: 'Delete member'}).click();
|
2022-11-24 18:11:40 +03:00
|
|
|
await page
|
|
|
|
.locator('.modal-content')
|
2022-12-01 03:34:19 +03:00
|
|
|
.filter({hasText: 'Delete member'})
|
2022-11-24 18:11:40 +03:00
|
|
|
.first()
|
2022-12-01 03:34:19 +03:00
|
|
|
.getByRole('button', {name: 'Delete member'})
|
2022-11-24 18:11:40 +03:00
|
|
|
.click();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connect from Stripe using the UI, disconnecting if necessary
|
|
|
|
* @param {import('@playwright/test').Page} page
|
|
|
|
* @param {object} tier
|
|
|
|
* @param {string} tier.name
|
|
|
|
* @param {number} tier.monthlyPrice
|
|
|
|
* @param {number} tier.yearlyPrice
|
|
|
|
*/
|
|
|
|
const createTier = async (page, {name, monthlyPrice, yearlyPrice}) => {
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.locator('.gh-nav a[href="#/settings/"]').click();
|
|
|
|
await page.locator('.gh-setting-group').filter({hasText: 'Membership'}).click();
|
2022-11-24 18:11:40 +03:00
|
|
|
// Expand the premium tier list
|
|
|
|
await page.getByRole('button', {name: 'Expand'}).nth(1).click({
|
2022-12-01 15:30:43 +03:00
|
|
|
delay: 500 // TODO: Figure out how to prevent this from opening with an empty list without using delay
|
2022-11-24 18:11:40 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// Archive if already exists
|
2022-12-01 03:34:19 +03:00
|
|
|
while (await page.locator('.gh-tier-card').filter({hasText: name}).first().isVisible()) {
|
2022-11-24 18:11:40 +03:00
|
|
|
const tierCard = page.locator('.gh-tier-card').filter({hasText: name}).first();
|
|
|
|
await tierCard.locator('.gh-tier-card-actions-button').click();
|
|
|
|
await tierCard.getByRole('button', {name: 'Archive'}).click();
|
|
|
|
await page.locator('.modal-content').getByRole('button', {name: 'Archive'}).click();
|
2022-12-01 15:30:43 +03:00
|
|
|
await page.locator('.modal-content').waitFor({state: 'detached', timeout: 1000});
|
2022-11-24 18:11:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
await page.locator('.gh-btn-add-tier').click();
|
2022-12-01 03:34:19 +03:00
|
|
|
const modal = page.locator('.modal-content');
|
|
|
|
await modal.locator('input#name').first().fill(name);
|
|
|
|
await modal.locator('#monthlyPrice').fill(`${monthlyPrice}`);
|
|
|
|
await modal.locator('#yearlyPrice').fill(`${yearlyPrice}`);
|
|
|
|
await modal.getByRole('button', {name: 'Add tier'}).click();
|
|
|
|
await page.waitForSelector('.modal-content input#name', {state: 'detached'});
|
|
|
|
|
|
|
|
await page.getByRole('button', {name: 'Customize Portal'}).click();
|
|
|
|
const portalSettings = page.locator('.modal-content').filter({hasText: 'Portal settings'});
|
|
|
|
if (!await portalSettings.locator('label').filter({hasText: name}).locator('input').first().isChecked()) {
|
|
|
|
await portalSettings.locator('label').filter({hasText: name}).locator('span').first().click();
|
|
|
|
}
|
|
|
|
if (!await portalSettings.locator('label').filter({hasText: 'Monthly'}).locator('input').first().isChecked()) {
|
|
|
|
await portalSettings.locator('label').filter({hasText: 'Monthly'}).locator('span').first().click();
|
|
|
|
}
|
|
|
|
if (!await portalSettings.locator('label').filter({hasText: 'Yearly'}).locator('input').first().isChecked()) {
|
|
|
|
await portalSettings.locator('label').filter({hasText: 'Yearly'}).locator('span').first().click();
|
|
|
|
}
|
|
|
|
await portalSettings.getByRole('button', {name: 'Save and close'}).click();
|
|
|
|
await page.waitForSelector('.gh-portal-settings', {state: 'detached'});
|
2022-11-24 18:11:40 +03:00
|
|
|
};
|
|
|
|
|
2022-11-24 20:46:58 +03:00
|
|
|
/**
|
|
|
|
* 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');
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.locator('.gh-nav a[href="#/offers/"]').click();
|
2022-11-24 20:46:58 +03:00
|
|
|
|
|
|
|
// 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([
|
2022-12-01 03:34:19 +03:00
|
|
|
page.locator('.gh-offers-list .gh-list-header').filter({hasText: 'active'}).waitFor({state: 'visible', timeout: 1000}).then(() => true),
|
|
|
|
page.locator('.gh-offers-list-cta').waitFor({state: 'visible', timeout: 1000}).then(() => false)
|
|
|
|
]).catch(() => false)) {
|
|
|
|
const listItem = page.locator('.gh-offers-list .gh-list-row:not(.header)').first();
|
|
|
|
await listItem.locator('a[href^="#/offers/"]').last().click();
|
2022-11-24 20:46:58 +03:00
|
|
|
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
|
2022-12-01 03:34:19 +03:00
|
|
|
const statusDropdown = await page.getByRole('button', {name: 'Archived offers'});
|
2022-11-24 20:46:58 +03:00
|
|
|
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();
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.locator('input#name').fill(offerName);
|
2022-11-24 20:46:58 +03:00
|
|
|
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
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.getByRole('button', {name: 'Saved'}).waitFor({
|
2022-11-24 20:46:58 +03:00
|
|
|
state: 'visible',
|
|
|
|
timeout: 1000
|
|
|
|
});
|
2022-12-01 03:34:19 +03:00
|
|
|
await page.locator('.gh-nav a[href="#/offers/"]').click();
|
2022-11-24 20:46:58 +03:00
|
|
|
|
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2022-11-24 18:11:40 +03:00
|
|
|
module.exports = {
|
|
|
|
setupGhost,
|
2022-12-01 15:05:20 +03:00
|
|
|
setupStripe,
|
2022-12-01 03:34:19 +03:00
|
|
|
deleteAllMembers,
|
2022-11-24 20:46:58 +03:00
|
|
|
createTier,
|
|
|
|
createOffer,
|
|
|
|
completeStripeSubscription
|
2022-11-24 18:11:40 +03:00
|
|
|
};
|