Ghost/ghost/core/test/e2e-browser/admin/members.spec.js

347 lines
20 KiB
JavaScript
Raw Normal View History

const {expect} = require('@playwright/test');
const test = require('../fixtures/ghost-test');
const {createMember, deleteAllMembers} = require('../utils/e2e-browser-utils');
const fs = require('fs');
test.describe('Admin', () => {
test.describe('Members', () => {
test.describe.configure({retries: 1, mode: 'serial'});
test('A member can be created', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('a[href="#/members/new/"] span');
await sharedPage.locator('a[href="#/members/new/"] span:has-text("New member")').click();
await sharedPage.waitForSelector('input[name="name"]');
let name = 'Test Member';
let email = 'tester@testmember.com';
let note = 'This is a test member';
let label = 'Test Label';
await sharedPage.fill('input[name="name"]', name);
await sharedPage.fill('input[name="email"]', email);
await sharedPage.fill('textarea[name="note"]', note);
await sharedPage.locator('label:has-text("Labels") + div').click();
await sharedPage.keyboard.type(label);
await sharedPage.keyboard.press('Tab');
await sharedPage.locator('button span:has-text("Save")').click();
await sharedPage.waitForSelector('button span:has-text("Saved")');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
// check number of members
await expect(sharedPage.locator('[data-test-list="members-list-item"]')).toHaveCount(1);
const member = sharedPage.locator('tbody > tr > a > div > div > h3').nth(0);
await expect(member).toHaveText(name);
const memberEmail = sharedPage.locator('tbody > tr > a > div > div > p').nth(0);
await expect(memberEmail).toHaveText(email);
});
test('A member cannot be created with invalid email', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('a[href="#/members/new/"] span');
await sharedPage.locator('a[href="#/members/new/"] span:has-text("New member")').click();
await sharedPage.waitForSelector('input[name="name"]');
let name = 'Test Member';
let email = 'tester+invalid@testmember.com<6F>';
let note = 'This is a test member';
let label = 'Test Label';
await sharedPage.fill('input[name="name"]', name);
await sharedPage.fill('input[name="email"]', email);
await sharedPage.fill('textarea[name="note"]', note);
await sharedPage.locator('label:has-text("Labels") + div').click();
await sharedPage.keyboard.type(label);
await sharedPage.keyboard.press('Tab');
await sharedPage.locator('button span:has-text("Save")').click();
await sharedPage.waitForSelector('button span:has-text("Retry")');
// check we are unable to save member with invalid email
await expect(sharedPage.locator('button span:has-text("Retry")')).toBeVisible();
await expect(sharedPage.locator('text=Invalid Email')).toBeVisible();
});
test('A member can be edited', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.locator('tbody > tr > a').nth(0).click();
await sharedPage.waitForSelector('input[name="name"]');
let name = 'Test Member Edited';
let email = 'tester.edited@example.com';
let note = 'This is an edited test member';
await sharedPage.fill('input[name="name"]', name);
await sharedPage.fill('input[name="email"]', email);
await sharedPage.fill('textarea[name="note"]', note);
await sharedPage.locator('label:has-text("Labels") + div').click();
await sharedPage.keyboard.press('Backspace');
await sharedPage.locator('body').click(); // this is to close the dropdown & lose focus
await sharedPage.locator('input[name="subscribed"] + span').click();
await sharedPage.locator('button span:has-text("Save")').click();
await sharedPage.waitForSelector('button span:has-text("Saved")');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
// check number of members
await expect(sharedPage.locator('[data-test-list="members-list-item"]')).toHaveCount(1);
const member = sharedPage.locator('tbody > tr > a > div > div > h3').nth(0);
await expect(member).toHaveText(name);
const memberEmail = sharedPage.locator('tbody > tr > a > div > div > p').nth(0);
await expect(memberEmail).toHaveText(email);
});
test('A member can be impersonated', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.locator('tbody > tr > a').nth(0).click();
await sharedPage.waitForSelector('[data-test-button="member-actions"]');
await sharedPage.locator('[data-test-button="member-actions"]').click();
await sharedPage.getByRole('button', {name: 'Impersonate'}).click();
await sharedPage.getByRole('button', {name: 'Copy link'}).click();
await sharedPage.waitForSelector('button span:has-text("Link copied")');
// get value from input because we don't have access to the clipboard during headless testing
const elem = await sharedPage.$('input[name="member-signin-url"]');
const link = await elem.inputValue();
await sharedPage.goto(link);
await sharedPage.frameLocator('#ghost-portal-root iframe[title="portal-trigger"]').locator('div').nth(1).click();
const title = await sharedPage.frameLocator('#ghost-portal-root div iframe[title="portal-popup"]').locator('h2').innerText();
await expect(title).toEqual('Your account'); // this is the title of the popup when member is logged in
});
test('A member can be deleted', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.locator('tbody > tr > a').nth(0).click();
await sharedPage.waitForSelector('[data-test-button="member-actions"]');
await sharedPage.locator('[data-test-button="member-actions"]').click();
await sharedPage.getByRole('button', {name: 'Delete member'}).click();
await sharedPage.locator('button[data-test-button="confirm"] span:has-text("Delete member")').click();
// should have no members now, so we should see the empty state
expect(await sharedPage.locator('div h4:has-text("Start building your audience")')).not.toBeNull();
});
const membersFixture = [
{
name: 'Test Member 1',
email: 'test@member1.com',
note: 'This is a test member',
label: 'old'
},
{
name: 'Test Member 2',
email: 'test@member2.com',
note: 'This is a test member',
label: 'old'
},
{
name: 'Test Member 3',
email: 'test@member3.com',
note: 'This is a test member',
label: 'old'
},
{
name: 'Sashi',
email: 'test@member4.com',
note: 'This is a test member',
label: 'dog'
},
{
name: 'Mia',
email: 'test@member5.com',
note: 'This is a test member',
label: 'dog'
},
{
name: 'Minki',
email: 'test@member6.com',
note: 'This is a test member',
label: 'dog'
}
];
test('All members can be exported', async ({sharedPage}) => {
// adds 6 members, 3 with the same label
for (let member of membersFixture) {
await createMember(sharedPage, member);
}
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('button[data-test-button="members-actions"]');
await sharedPage.locator('button[data-test-button="members-actions"]').click();
await sharedPage.waitForSelector('button[data-test-button="export-members"]');
const [download] = await Promise.all([
sharedPage.waitForEvent('download'),
sharedPage.locator('button[data-test-button="export-members"]').click()
]);
const filename = await download.suggestedFilename();
expect(filename).toContain('.csv');
const csv = await download.path();
let csvContents = await fs.readFileSync(csv).toString();
expect(csvContents).toMatch(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/);
membersFixture.forEach((member) => {
expect(csvContents).toMatch(member.name);
expect(csvContents).toMatch(member.email);
expect(csvContents).toMatch(member.note);
expect(csvContents).toMatch(member.label);
});
// expect(csvContents).toMatch('Test Label'); we deleted the label in a previous test so it's not in this the export
const countIds = csvContents.match(/[a-z0-9]{24}/gm).length;
expect(countIds).toEqual(6);
const countTimestamps = csvContents.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/gm).length;
expect(countTimestamps).toEqual(6);
const countRows = csvContents.match(/(?:"(?:[^"]|"")*"|[^,\n]*)(?:,(?:"(?:[^"]|"")*"|[^,\n]*))*\n/g).length;
expect(countRows).toEqual(6);
const csvRegex = /^[^",]+((?<=[,\n])|(?=[,\n]))|[^",]+/gm;
expect(csvContents).toMatch(csvRegex);
});
test('A filtered list of members can be exported', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('button[data-test-button="members-actions"]');
await sharedPage.locator('button[data-test-button="members-actions"]').click();
await sharedPage.waitForSelector('div[data-test-button="members-filter-actions"]');
await sharedPage.locator('div[data-test-button="members-filter-actions"]').click();
await sharedPage.locator('select[data-test-select="members-filter"]').click();
await sharedPage.locator('select[data-test-select="members-filter"]').selectOption('label');
await sharedPage.locator('div[data-test-members-filter="0"] > div > div').click();
await sharedPage.locator('span[data-test-label-filter="dog"]').click();
await sharedPage.keyboard.press('Tab');
await sharedPage.locator('button[data-test-button="members-apply-filter"]').click();
await sharedPage.locator('button[data-test-button="members-actions"]').click();
const exportButton = await sharedPage.locator('button[data-test-button="export-members"] > span').innerText();
expect(exportButton).toEqual('Export selected members (3)');
await sharedPage.waitForSelector('button[data-test-button="export-members"]');
const [download] = await Promise.all([
sharedPage.waitForEvent('download'),
sharedPage.locator('button[data-test-button="export-members"]').click()
]);
const filename = await download.suggestedFilename();
expect(filename).toContain('.csv');
const csv = await download.path();
let csvContents = await fs.readFileSync(csv).toString();
expect(csvContents).toMatch(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/);
// filter memberFixtures to only include members with the label 'dog'
const filteredMembersFixture = membersFixture.filter((member) => {
return member.label === 'dog';
});
filteredMembersFixture.forEach((member) => {
expect(csvContents).toMatch(member.name);
expect(csvContents).toMatch(member.email);
expect(csvContents).toMatch(member.note);
expect(csvContents).toMatch('dog');
});
const countIds = csvContents.match(/[a-z0-9]{24}/gm).length;
expect(countIds).toEqual(3);
const countTimestamps = csvContents.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/gm).length;
expect(countTimestamps).toEqual(3);
const countRows = csvContents.match(/(?:"(?:[^"]|"")*"|[^,\n]*)(?:,(?:"(?:[^"]|"")*"|[^,\n]*))*\n/g).length;
expect(countRows).toEqual(3);
const csvRegex = /^[^",]+((?<=[,\n])|(?=[,\n]))|[^",]+/gm;
expect(csvContents).toMatch(csvRegex);
});
// saves time by going directly to the members page with the label filter applied
let labelFilterUrl;
test('A filtered list of members can have a label added to them', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('button[data-test-button="members-actions"]');
await sharedPage.locator('button[data-test-button="members-actions"]').click();
await sharedPage.waitForSelector('div[data-test-button="members-filter-actions"]');
await sharedPage.locator('div[data-test-button="members-filter-actions"]').click();
await sharedPage.locator('select[data-test-select="members-filter"]').click();
await sharedPage.locator('select[data-test-select="members-filter"]').selectOption('label');
await sharedPage.locator('div[data-test-members-filter="0"] > div > div').click();
await sharedPage.locator('span[data-test-label-filter="old"]').click(); // this label should only be on 3 members
await sharedPage.keyboard.press('Tab');
await sharedPage.locator('button[data-test-button="members-apply-filter"]').click();
await sharedPage.waitForSelector('button[data-test-button="members-actions"]');
await sharedPage.locator('button[data-test-button="members-actions"]').click();
await sharedPage.locator('button[data-test-button="add-label-selected"]').click();
await sharedPage.locator('div[data-test-state="add-label-unconfirmed"] > span > select').selectOption({label: 'Test Label'});
const members = await sharedPage.locator('div[data-test-state="add-label-unconfirmed"] > p > span[data-test-text="member-count"]').innerText();
expect(members).toEqual('3 members');
await sharedPage.locator('button[data-test-button="confirm"]').click();
await sharedPage.waitForSelector('div[data-test-state="add-complete"]');
const success = await sharedPage.locator('div[data-test-state="add-complete"] > div > p').innerText();
expect(success).toEqual('Label added to 3 members successfully');
labelFilterUrl = await sharedPage.url();
});
test('A filtered list of members can have a label removed from them', async ({sharedPage}) => {
await sharedPage.goto(labelFilterUrl);
await sharedPage.waitForSelector('button[data-test-button="members-actions"]');
await sharedPage.locator('button[data-test-button="members-actions"]').click();
await sharedPage.waitForSelector('button[data-test-button="remove-label-selected"]');
await sharedPage.locator('button[data-test-button="remove-label-selected"]').click();
await sharedPage.locator('div[data-test-state="remove-label-unconfirmed"] > span > select').selectOption({label: 'old'});
await sharedPage.locator('button[data-test-button="confirm"]').click();
const success = await sharedPage.locator('div[data-test-state="remove-complete"] > div > p').innerText();
expect(success).toEqual('Label removed from 3 members successfully');
});
test('A member can be granted a comp in admin', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await deleteAllMembers(sharedPage);
// create a new member with a comped plan
await createMember(sharedPage, {
name: 'Test Member 1',
email: 'test@member1.com',
note: 'This is a test member',
label: 'Test Label',
compedPlan: 'The Local Test'
});
// open the impersonate modal
await sharedPage.locator('[data-test-button="member-actions"]').click();
await sharedPage.getByRole('button', {name: 'Impersonate'}).click();
await sharedPage.getByRole('button', {name: 'Copy link'}).click();
await sharedPage.waitForSelector('button span:has-text("Link copied")');
// get value from input because we don't have access to the clipboard during headless testing
const elem = await sharedPage.$('input[name="member-signin-url"]');
const link = await elem.inputValue();
// go to the frontend with the impersonate link
await sharedPage.goto(link);
// click the paid-only post
await sharedPage.locator('.gh-card-link[href="/sell/"]').click();
// check for content CTA and expect it to be zero
await expect(sharedPage.locator('.gh-post-upgrade-cta')).toHaveCount(0);
});
test('An existing member cannot be saved with invalid email address', async ({sharedPage}) => {
await sharedPage.goto('/ghost');
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
await sharedPage.waitForSelector('a[href="#/members/new/"] span');
await sharedPage.locator('a[href="#/members/new/"] span:has-text("New member")').click();
await sharedPage.waitForSelector('input[name="name"]');
let name = 'Test Member';
let email = 'tester+invalid@example.com';
let note = 'This is a test member';
let label = 'Test Label';
await sharedPage.fill('input[name="name"]', name);
await sharedPage.fill('input[name="email"]', email);
await sharedPage.fill('textarea[name="note"]', note);
await sharedPage.locator('label:has-text("Labels") + div').click();
await sharedPage.keyboard.type(label);
await sharedPage.keyboard.press('Tab');
await sharedPage.locator('button span:has-text("Save")').click();
await sharedPage.waitForSelector('button span:has-text("Saved")');
// Update email to invalid and try saving
let updatedEmail = 'tester+invalid@example.com<6F>';
await sharedPage.fill('input[name="email"]', updatedEmail);
await sharedPage.waitForSelector('button span:has-text("Save")');
await sharedPage.locator('button span:has-text("Save")').click();
await sharedPage.waitForSelector('button span:has-text("Retry")');
// check we are unable to save member with invalid email
await expect(sharedPage.locator('button span:has-text("Retry")')).toBeVisible();
await expect(sharedPage.locator('text=Invalid Email')).toBeVisible();
});
});
});