Ghost/apps/admin-x-settings/test/acceptance/email/newsletters.test.ts
Daniël van der Winden 0aef3d3bee
Error copy updates for Newsletter settings (#20559)
Fixes
https://linear.app/tryghost/issue/DES-550/mechanical-error-message-in-newsletter-detail-modal

Errors in the newsletter detail modal sounded mechanical and were
inconsistent with other error messages. That is now fixed, and they
sound more human.
2024-07-08 15:33:26 +00:00

436 lines
20 KiB
TypeScript

import {chooseOptionInSelect, limitRequests, mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance';
import {expect, test} from '@playwright/test';
import {globalDataRequests} from '../../utils/acceptance';
test.describe('Newsletter settings', async () => {
test('Supports creating a new newsletter', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
addNewsletter: {method: 'POST', path: '/newsletters/?opt_in_existing=true&include=count.active_members%2Ccount.posts', response: {newsletters: [{
id: 'new-newsletter',
name: 'New newsletter',
description: null,
count: {
active_members: 0,
posts: 0
}
}]}}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByRole('button', {name: 'Add newsletter'}).click();
const modal = page.getByTestId('add-newsletter-modal');
await modal.getByRole('button', {name: 'Create'}).click();
await expect(modal).toHaveText(/A name is required for your newsletter/);
// Shouldn't be necessary, but without these Playwright doesn't click Create the second time for some reason
await modal.getByRole('button', {name: 'Cancel'}).click();
await section.getByRole('button', {name: 'Add newsletter'}).click();
await modal.getByLabel('Name').fill('New newsletter');
await modal.getByRole('button', {name: 'Create'}).click();
await expect(page.getByTestId('newsletter-modal')).toHaveCount(1);
expect(lastApiRequests.addNewsletter?.body).toMatchObject({
newsletters: [{
name: 'New newsletter'
}]
});
});
test('Supports updating a newsletter', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [{
...responseFixtures.newsletters.newsletters[0],
name: 'Updated newsletter',
body_font_category: 'sans_serif'
}]
}}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
await modal.getByPlaceholder('Weekly Roundup').fill('');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal).toHaveText(/A name is required for your newsletter/);
await modal.getByPlaceholder('Weekly Roundup').fill('Updated newsletter');
await modal.getByRole('tab', {name: 'Design'}).click();
await chooseOptionInSelect(modal.getByTestId('body-font-select'), 'Clean sans-serif');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(section.getByText('Updated newsletter')).toHaveCount(1);
expect(lastApiRequests.editNewsletter?.body).toMatchObject({
newsletters: [{
id: responseFixtures.newsletters.newsletters[0].id,
name: 'Updated newsletter',
body_font_category: 'sans_serif'
}]
});
});
test.describe('Email addresses', async () => {
test.describe('For self-hosters', async () => {
test('Displays a prompt when email verification is required', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [responseFixtures.newsletters.newsletters[0]],
meta: {
sent_email_verification: ['sender_email']
}
}}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
await modal.getByLabel('Sender email').fill('not-an-email');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal).toHaveText(/Enter a valid email address/);
await modal.getByLabel('Sender email').fill('test@test.com');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(page.getByTestId('toast-info')).toHaveCount(1);
await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/);
});
});
test.describe('For Ghost (Pro) users without custom domain', () => {
test('Does not allow the Sender email address to be edited', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
browseConfig: {
...globalDataRequests.browseConfig,
response: {
config: {
...responseFixtures.config.config,
hostSettings: {
managedEmail: {
enabled: true
}
}
}
}
}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
const senderEmailField = modal.getByLabel('Sender email');
// Test that there is no input field near "Sender email"
const parentElementLocator = senderEmailField.locator('xpath=..');
const inputElementsNearby = await parentElementLocator.locator('input').count();
expect(inputElementsNearby).toBe(0);
});
test('Allow full customisation of the reply-to address', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [responseFixtures.newsletters.newsletters[0]],
meta: {
sent_email_verification: ['sender_reply_to']
}
}},
browseConfig: {
...globalDataRequests.browseConfig,
response: {
config: {
...responseFixtures.config.config,
hostSettings: {
managedEmail: {
enabled: true
}
}
}
}
}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
const replyToEmail = modal.getByLabel('Reply-to email');
await replyToEmail.fill('not-an-email');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal).toHaveText(/Enter a valid email address/);
await replyToEmail.fill('test@test.com');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(page.getByTestId('toast-info')).toHaveCount(1);
await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/);
});
});
test.describe('For Ghost (Pro) users with custom sending domain', () => {
test('The sender email address can be changed partially (username but not domain name)', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [responseFixtures.newsletters.newsletters[0]],
meta: {
sent_email_verification: []
}
}},
browseConfig: {
...globalDataRequests.browseConfig,
response: {
config: {
...responseFixtures.config.config,
hostSettings: {
managedEmail: {
enabled: true,
sendingDomain: 'customdomain.com'
}
}
}
}
}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
const senderEmail = modal.getByLabel('Sender email');
// Error case #1: add invalid email address
await senderEmail.fill('Harry Potter');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal).toHaveText(/Enter a valid email address/);
// Error case #2: the sender email address doesn't match the custom sending domain
await senderEmail.fill('harry@potter.com');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(modal).toHaveText(/Email address must end with @customdomain.com/);
// But can have any address on the same domain, without verification
await senderEmail.fill('harry@customdomain.com');
await modal.getByRole('button', {name: 'Save'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveCount(0);
});
test('Allow full customisation of the reply-to address, with verification', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [responseFixtures.newsletters.newsletters[0]],
meta: {
sent_email_verification: ['sender_reply_to']
}
}},
browseConfig: {
...globalDataRequests.browseConfig,
response: {
config: {
...responseFixtures.config.config,
hostSettings: {
managedEmail: {
enabled: true,
sendingDomain: 'customdomain.com'
}
}
}
}
}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
const replyToEmail = modal.getByLabel('Reply-to email');
// Full flexibility for the reply-to address
await replyToEmail.fill('hermione@granger.com');
expect(await replyToEmail.inputValue()).toBe('hermione@granger.com');
// There is a verification popup for the new reply-to address
await modal.getByRole('button', {name: 'Save'}).click();
await expect(page.getByTestId('toast-info')).toHaveCount(1);
await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/);
});
});
});
test('Supports archiving newsletters', async ({page}) => {
const activate = await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[1].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [{
...responseFixtures.newsletters.newsletters[1],
status: 'active'
}]
}}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByRole('tab', {name: 'Archived'}).click();
await section.getByText('Average newsletter').hover();
await section.getByRole('button', {name: 'Edit'}).click();
const archivedNewsletterModal = page.getByTestId('newsletter-modal');
await archivedNewsletterModal.getByRole('button', {name: 'Reactivate newsletter'}).click();
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Reactivate'}).click();
await archivedNewsletterModal.getByRole('button', {name: 'Close'}).click();
await section.getByRole('tab', {name: 'Active'}).click();
await expect(section.getByText('Awesome newsletter')).toHaveCount(1);
await expect(section.getByText('Average newsletter')).toHaveCount(1);
expect(activate.lastApiRequests.editNewsletter?.body).toMatchObject({
newsletters: [{
id: responseFixtures.newsletters.newsletters[1].id,
status: 'active'
}]
});
const archive = await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
newsletters: [{
...responseFixtures.newsletters.newsletters[0],
status: 'archived'
}]
}}
}});
await section.getByText('Awesome newsletter').hover();
await section.getByRole('button', {name: 'Edit'}).click();
const activeNewsletterModal = page.getByTestId('newsletter-modal');
await activeNewsletterModal.getByRole('button', {name: 'Archive newsletter'}).click();
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Archive'}).click();
await activeNewsletterModal.getByRole('button', {name: 'Close'}).click();
await section.getByRole('tab', {name: 'Archived'}).click();
await expect(section.getByText('Awesome newsletter')).toHaveCount(1);
expect(archive.lastApiRequests.editNewsletter?.body).toMatchObject({
newsletters: [{
id: responseFixtures.newsletters.newsletters[0].id,
status: 'archived'
}]
});
});
test('Limits the number of newsletters', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
...limitRequests,
browseConfig: {
...globalDataRequests.browseConfig,
response: {
config: {
...responseFixtures.config.config,
hostSettings: {
limits: {
newsletters: {
max: 1,
error: 'Your plan supports up to {{max}} newsletters. Please upgrade to add more.'
}
}
}
}
}
},
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByRole('button', {name: 'Add newsletter'}).click();
await expect(page.getByTestId('limit-modal')).toHaveText(/Your plan supports up to 1 newsletters/);
await page.getByTestId('limit-modal').getByRole('button', {name: 'Cancel'}).click();
await section.getByRole('tab', {name: 'Archived'}).click();
await section.getByText('Average newsletter').hover();
await section.getByRole('button', {name: 'Edit'}).click();
const newsletterModal = page.getByTestId('newsletter-modal');
await newsletterModal.getByRole('button', {name: 'Reactivate newsletter'}).click();
await expect(page.getByTestId('limit-modal')).toHaveText(/Your plan supports up to 1 newsletters/);
});
test('Warns when leaving without saving', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[1].id}/?include=count.active_members%2Ccount.posts`, response: responseFixtures.newsletters}
}});
await page.goto('/');
const section = page.getByTestId('newsletters');
await section.getByText('Awesome newsletter').click();
const modal = page.getByTestId('newsletter-modal');
await modal.getByPlaceholder('Weekly Roundup').fill('New title');
await modal.getByRole('button', {name: 'Close'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Leave'}).click();
await expect(modal).toBeHidden();
expect(lastApiRequests.editNewsletter).toBeUndefined();
});
});