mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 10:53:34 +03:00
3d6753a54a
refs https://github.com/TryGhost/Team/issues/2371 A member can be granted a comp in admin, that account should be able to access paid content.
245 lines
13 KiB
JavaScript
245 lines
13 KiB
JavaScript
const {expect, test} = require('@playwright/test');
|
|
const {createMember, deleteAllMembers} = require('../utils/e2e-browser-utils');
|
|
const fs = require('fs');
|
|
|
|
test.describe('Admin', () => {
|
|
test.describe('Members', () => {
|
|
test('A member can be created', async ({page}) => {
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.waitForSelector('a[href="#/members/new/"] span');
|
|
await page.locator('a[href="#/members/new/"] span:has-text("New member")').click();
|
|
await page.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 page.fill('input[name="name"]', name);
|
|
await page.fill('input[name="email"]', email);
|
|
await page.fill('textarea[name="note"]', note);
|
|
await page.locator('label:has-text("Labels") + div').click();
|
|
await page.keyboard.type(label);
|
|
await page.keyboard.press('Tab');
|
|
await page.locator('button span:has-text("Save")').click();
|
|
await page.waitForSelector('button span:has-text("Saved")');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
const count = await page.locator('tbody > tr').count();
|
|
expect(count).toBe(1);
|
|
const member = page.locator('tbody > tr > a > div > div > h3').nth(0);
|
|
await expect(member).toHaveText(name);
|
|
const memberEmail = page.locator('tbody > tr > a > div > div > p').nth(0);
|
|
await expect(memberEmail).toHaveText(email);
|
|
});
|
|
|
|
test('A member can be edited', async ({page}) => {
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.locator('tbody > tr > a').nth(0).click();
|
|
await page.waitForSelector('input[name="name"]');
|
|
let name = 'Test Member Edited';
|
|
let email = 'tester.edited@example.com';
|
|
let note = 'This is an edited test member';
|
|
await page.fill('input[name="name"]', name);
|
|
await page.fill('input[name="email"]', email);
|
|
await page.fill('textarea[name="note"]', note);
|
|
await page.locator('label:has-text("Labels") + div').click();
|
|
await page.keyboard.press('Backspace');
|
|
await page.locator('body').click(); // this is to close the dropdown & lose focus
|
|
await page.locator('input[name="subscribed"] + span').click();
|
|
await page.locator('button span:has-text("Save")').click();
|
|
await page.waitForSelector('button span:has-text("Saved")');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
const count = await page.locator('tbody > tr').count();
|
|
expect(count).toBe(1);
|
|
const member = page.locator('tbody > tr > a > div > div > h3').nth(0);
|
|
await expect(member).toHaveText(name);
|
|
const memberEmail = page.locator('tbody > tr > a > div > div > p').nth(0);
|
|
await expect(memberEmail).toHaveText(email);
|
|
});
|
|
|
|
test('A member can be impersonated', async ({page}) => {
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.locator('tbody > tr > a').nth(0).click();
|
|
await page.waitForSelector('[data-test-button="member-actions"]');
|
|
await page.locator('[data-test-button="member-actions"]').click();
|
|
await page.getByRole('button', {name: 'Impersonate'}).click();
|
|
await page.getByRole('button', {name: 'Copy link'}).click();
|
|
await page.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 page.$('input[name="member-signin-url"]');
|
|
const link = await elem.inputValue();
|
|
await page.goto(link);
|
|
await page.frameLocator('#ghost-portal-root iframe[title="portal-trigger"]').locator('div').nth(1).click();
|
|
const title = await page.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 ({page}) => {
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.locator('tbody > tr > a').nth(0).click();
|
|
await page.waitForSelector('[data-test-button="member-actions"]');
|
|
await page.locator('[data-test-button="member-actions"]').click();
|
|
await page.getByRole('button', {name: 'Delete member'}).click();
|
|
await page.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 page.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: 'Test Label'
|
|
},
|
|
{
|
|
name: 'Test Member 2',
|
|
email: 'test@member2.com',
|
|
note: 'This is a test member',
|
|
label: 'Test Label'
|
|
},
|
|
{
|
|
name: 'Test Member 3',
|
|
email: 'test@member3.com',
|
|
note: 'This is a test member',
|
|
label: 'Test Label'
|
|
},
|
|
{
|
|
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 ({page}) => {
|
|
// adds 6 members, 3 with the same label
|
|
for (let member of membersFixture) {
|
|
await createMember(page, member);
|
|
}
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.waitForSelector('button[data-test-button="members-actions"]');
|
|
await page.locator('button[data-test-button="members-actions"]').click();
|
|
await page.waitForSelector('button[data-test-button="export-members"]');
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent('download'),
|
|
page.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 ({page}) => {
|
|
await page.goto('/ghost');
|
|
await page.locator('.gh-nav a[href="#/members/"]').click();
|
|
await page.waitForSelector('button[data-test-button="members-actions"]');
|
|
await page.locator('button[data-test-button="members-actions"]').click();
|
|
await page.waitForSelector('div[data-test-button="members-filter-actions"]');
|
|
await page.locator('div[data-test-button="members-filter-actions"]').click();
|
|
await page.locator('select[data-test-select="members-filter"]').click();
|
|
await page.locator('select[data-test-select="members-filter"]').selectOption('label');
|
|
await page.locator('div[data-test-members-filter="0"] > div > div').click();
|
|
await page.locator('span[data-test-label-filter="dog"]').click();
|
|
await page.keyboard.press('Tab');
|
|
await page.locator('button[data-test-button="members-apply-filter"]').click();
|
|
await page.locator('button[data-test-button="members-actions"]').click();
|
|
const exportButton = await page.locator('button[data-test-button="export-members"] > span').innerText();
|
|
expect(exportButton).toEqual('Export selected members (3)');
|
|
await page.waitForSelector('button[data-test-button="export-members"]');
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent('download'),
|
|
page.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);
|
|
});
|
|
|
|
test('A member can be granted a comp in admin', async ({page}) => {
|
|
page.goto('/ghost');
|
|
await deleteAllMembers(page);
|
|
|
|
// create a new member with a comped plan
|
|
await createMember(page, {
|
|
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 page.locator('[data-test-button="member-actions"]').click();
|
|
await page.getByRole('button', {name: 'Impersonate'}).click();
|
|
await page.getByRole('button', {name: 'Copy link'}).click();
|
|
await page.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 page.$('input[name="member-signin-url"]');
|
|
const link = await elem.inputValue();
|
|
|
|
// go to the frontend with the impersonate link
|
|
await page.goto(link);
|
|
|
|
// click the paid-only post
|
|
await page.locator('.post-card-image-link[href="/sell/"]').click();
|
|
|
|
// check for content CTA and expect it to be zero
|
|
await expect(page.locator('.gh-post-upgrade-cta')).toHaveCount(0);
|
|
});
|
|
});
|
|
});
|