mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Added simple tests for design modal
refs https://github.com/TryGhost/Team/issues/3354
This commit is contained in:
parent
90c269275e
commit
c3f6d77fd9
@ -15,6 +15,7 @@ export interface ModalProps {
|
|||||||
*/
|
*/
|
||||||
size?: ModalSize;
|
size?: ModalSize;
|
||||||
|
|
||||||
|
testId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
okLabel?: string;
|
okLabel?: string;
|
||||||
okColor?: string;
|
okColor?: string;
|
||||||
@ -32,6 +33,7 @@ export interface ModalProps {
|
|||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({
|
const Modal: React.FC<ModalProps> = ({
|
||||||
size = 'md',
|
size = 'md',
|
||||||
|
testId,
|
||||||
title,
|
title,
|
||||||
okLabel = 'OK',
|
okLabel = 'OK',
|
||||||
cancelLabel = 'Cancel',
|
cancelLabel = 'Cancel',
|
||||||
@ -179,7 +181,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
'pointer-events-none fixed inset-0 z-0',
|
'pointer-events-none fixed inset-0 z-0',
|
||||||
backDrop && 'bg-[rgba(98,109,121,0.15)] backdrop-blur-[3px]'
|
backDrop && 'bg-[rgba(98,109,121,0.15)] backdrop-blur-[3px]'
|
||||||
)}></div>
|
)}></div>
|
||||||
<section className={modalClasses} style={modalStyles}>
|
<section className={modalClasses} data-testid={testId} style={modalStyles}>
|
||||||
<div className={contentClasses}>
|
<div className={contentClasses}>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
{title && <Heading level={4}>{title}</Heading>}
|
{title && <Heading level={4}>{title}</Heading>}
|
||||||
|
@ -10,6 +10,7 @@ import {IButton} from './Button';
|
|||||||
import {SelectOption} from './Select';
|
import {SelectOption} from './Select';
|
||||||
|
|
||||||
export interface PreviewModalProps {
|
export interface PreviewModalProps {
|
||||||
|
testId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
sidebar?: React.ReactNode;
|
sidebar?: React.ReactNode;
|
||||||
preview?: React.ReactNode;
|
preview?: React.ReactNode;
|
||||||
@ -31,6 +32,7 @@ export interface PreviewModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||||
|
testId,
|
||||||
title,
|
title,
|
||||||
sidebar,
|
sidebar,
|
||||||
preview,
|
preview,
|
||||||
@ -138,6 +140,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||||||
customFooter={(<></>)}
|
customFooter={(<></>)}
|
||||||
noPadding={true}
|
noPadding={true}
|
||||||
size='full'
|
size='full'
|
||||||
|
testId={testId}
|
||||||
title=''
|
title=''
|
||||||
>
|
>
|
||||||
<div className='flex h-full grow'>
|
<div className='flex h-full grow'>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export type Tab = {
|
export type Tab = {
|
||||||
id: string,
|
id: string,
|
||||||
@ -29,13 +30,25 @@ const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className='flex gap-5 border-b border-grey-300'>
|
<div className='flex gap-5 border-b border-grey-300' role='tablist'>
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button key={tab.id} className={`-m-b-px cursor-pointer appearance-none border-b-[3px] py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)] ${selectedTab === tab.id ? 'border-black font-bold' : 'border-transparent hover:border-grey-500'}`} id={tab.id} title={tab.title} type="button" onClick={handleTabChange}>{tab.title}</button>
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
aria-selected={selectedTab === tab.id}
|
||||||
|
className={clsx(
|
||||||
|
'-m-b-px cursor-pointer appearance-none border-b-[3px] py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
|
||||||
|
selectedTab === tab.id ? 'border-black font-bold' : 'border-transparent hover:border-grey-500'
|
||||||
|
)}
|
||||||
|
id={tab.id}
|
||||||
|
role='tab'
|
||||||
|
title={tab.title}
|
||||||
|
type="button"
|
||||||
|
onClick={handleTabChange}
|
||||||
|
>{tab.title}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<div key={tab.id} className={`${selectedTab === tab.id ? 'block' : 'hidden'}`}>
|
<div key={tab.id} className={`${selectedTab === tab.id ? 'block' : 'hidden'}`} role='tabpanel'>
|
||||||
<div>{tab.contents}</div>
|
<div>{tab.contents}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -5,7 +5,7 @@ import {Setting, SiteData} from '../../types/api';
|
|||||||
// Define the Settings Context
|
// Define the Settings Context
|
||||||
interface SettingsContextProps {
|
interface SettingsContextProps {
|
||||||
settings: Setting[] | null;
|
settings: Setting[] | null;
|
||||||
saveSettings: (updatedSettings: Setting[]) => Promise<void>;
|
saveSettings: (updatedSettings: Setting[]) => Promise<Setting[]>;
|
||||||
siteData: SiteData | null;
|
siteData: SiteData | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ interface SettingsProviderProps {
|
|||||||
const SettingsContext = createContext<SettingsContextProps>({
|
const SettingsContext = createContext<SettingsContextProps>({
|
||||||
settings: null,
|
settings: null,
|
||||||
siteData: null,
|
siteData: null,
|
||||||
saveSettings: async () => {}
|
saveSettings: async () => []
|
||||||
});
|
});
|
||||||
|
|
||||||
function serialiseSettingsData(settings: Setting[]): Setting[] {
|
function serialiseSettingsData(settings: Setting[]): Setting[] {
|
||||||
@ -100,16 +100,20 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
|||||||
fetchSettings();
|
fetchSettings();
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const saveSettings = useCallback(async (updatedSettings: Setting[]): Promise<void> => {
|
const saveSettings = useCallback(async (updatedSettings: Setting[]) => {
|
||||||
try {
|
try {
|
||||||
// handle transformation for settings before save
|
// handle transformation for settings before save
|
||||||
updatedSettings = deserializeSettings(updatedSettings);
|
updatedSettings = deserializeSettings(updatedSettings);
|
||||||
// Make an API call to save the updated settings
|
// Make an API call to save the updated settings
|
||||||
const data = await api.settings.edit(updatedSettings);
|
const data = await api.settings.edit(updatedSettings);
|
||||||
|
const newSettings = serialiseSettingsData(data.settings);
|
||||||
|
|
||||||
setSettings(serialiseSettingsData(data.settings));
|
setSettings(newSettings);
|
||||||
|
|
||||||
|
return newSettings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error in settings API
|
// Log error in settings API
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ const DesignModal: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const {settings} = useContext(SettingsContext);
|
const {settings, saveSettings} = useContext(SettingsContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState,
|
formState,
|
||||||
@ -80,8 +80,8 @@ const DesignModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formState.settings.some(setting => setting.dirty)) {
|
if (formState.settings.some(setting => setting.dirty)) {
|
||||||
const response = await api.settings.edit(formState.settings);
|
const newSettings = await saveSettings(formState.settings.filter(setting => setting.dirty));
|
||||||
updateForm(state => ({...state, settings: response.settings}));
|
updateForm(state => ({...state, settings: newSettings}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -127,7 +127,7 @@ const DesignModal: React.FC = () => {
|
|||||||
return <PreviewModalContent
|
return <PreviewModalContent
|
||||||
buttonsDisabled={saveState === 'saving'}
|
buttonsDisabled={saveState === 'saving'}
|
||||||
cancelLabel='Close'
|
cancelLabel='Close'
|
||||||
okLabel='Save'
|
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving ...' : 'Save')}
|
||||||
preview={
|
preview={
|
||||||
<ThemePreview
|
<ThemePreview
|
||||||
settings={{
|
settings={{
|
||||||
@ -148,6 +148,7 @@ const DesignModal: React.FC = () => {
|
|||||||
updateThemeSetting={updateThemeSetting}
|
updateThemeSetting={updateThemeSetting}
|
||||||
/>}
|
/>}
|
||||||
sidebarPadding={false}
|
sidebarPadding={false}
|
||||||
|
testId='design-modal'
|
||||||
title='Design'
|
title='Design'
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
if (saveState === 'unsaved') {
|
if (saveState === 'unsaved') {
|
||||||
|
@ -14,6 +14,7 @@ const DesignSetting: React.FC = () => {
|
|||||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||||
description="Customize your site and manage themes"
|
description="Customize your site and manage themes"
|
||||||
navid='branding-and-design'
|
navid='branding-and-design'
|
||||||
|
testId='design'
|
||||||
title="Branding and design"
|
title="Branding and design"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -115,7 +115,10 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef} height="100%" title="Site Preview"
|
ref={iframeRef}
|
||||||
|
data-testid="theme-preview"
|
||||||
|
height="100%"
|
||||||
|
title="Site Preview"
|
||||||
width="100%"
|
width="100%"
|
||||||
></iframe>
|
></iframe>
|
||||||
</>
|
</>
|
||||||
|
62
ghost/admin-x-settings/test/e2e/site/design.test.ts
Normal file
62
ghost/admin-x-settings/test/e2e/site/design.test.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {expect, test} from '@playwright/test';
|
||||||
|
import {mockApi} from '../../utils/e2e';
|
||||||
|
|
||||||
|
test.describe('Theme settings', async () => {
|
||||||
|
test('Editing brand settings', async ({page}) => {
|
||||||
|
const lastApiRequest = await mockApi({page, responses: {
|
||||||
|
previewHtml: {
|
||||||
|
homepage: '<html><head><style></style></head><body><div>homepage preview</div></body></html>'
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const section = page.getByTestId('design');
|
||||||
|
|
||||||
|
await section.getByRole('button', {name: 'Customize'}).click();
|
||||||
|
|
||||||
|
const modal = page.getByTestId('design-modal');
|
||||||
|
|
||||||
|
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
|
||||||
|
|
||||||
|
await modal.getByLabel('Site description').fill('new description');
|
||||||
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
|
||||||
|
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
|
||||||
|
|
||||||
|
expect(lastApiRequest.body).toEqual({
|
||||||
|
settings: [
|
||||||
|
{key: 'description', value: 'new description'}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Editing custom theme settings', async ({page}) => {
|
||||||
|
const lastApiRequest = await mockApi({page});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const section = page.getByTestId('design');
|
||||||
|
|
||||||
|
await section.getByRole('button', {name: 'Customize'}).click();
|
||||||
|
|
||||||
|
const modal = page.getByTestId('design-modal');
|
||||||
|
|
||||||
|
await modal.getByRole('tab', {name: 'Site wide'}).click();
|
||||||
|
|
||||||
|
await modal.getByLabel('Navigation layout').selectOption('Logo in the middle');
|
||||||
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
|
||||||
|
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
|
||||||
|
|
||||||
|
expect(lastApiRequest.body).toMatchObject({
|
||||||
|
custom_theme_settings: [
|
||||||
|
{key: 'navigation_color'},
|
||||||
|
{key: 'navigation_background_image'},
|
||||||
|
{key: 'navigation_layout', value: 'Logo in the middle'},
|
||||||
|
{key: 'show_publication_cover'},
|
||||||
|
{key: 'email_signup_text'}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -8,7 +8,8 @@ type LastApiRequest = {
|
|||||||
|
|
||||||
const responseFixtures = {
|
const responseFixtures = {
|
||||||
settings: JSON.parse(readFileSync(`${__dirname}/responses/settings.json`).toString()),
|
settings: JSON.parse(readFileSync(`${__dirname}/responses/settings.json`).toString()),
|
||||||
site: JSON.parse(readFileSync(`${__dirname}/responses/site.json`).toString())
|
site: JSON.parse(readFileSync(`${__dirname}/responses/site.json`).toString()),
|
||||||
|
custom_theme_settings: JSON.parse(readFileSync(`${__dirname}/responses/custom_theme_settings.json`).toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Responses {
|
interface Responses {
|
||||||
@ -22,6 +23,13 @@ interface Responses {
|
|||||||
images?: {
|
images?: {
|
||||||
upload?: any
|
upload?: any
|
||||||
}
|
}
|
||||||
|
custom_theme_settings?: {
|
||||||
|
browse?: any
|
||||||
|
edit?: any
|
||||||
|
}
|
||||||
|
previewHtml: {
|
||||||
|
homepage?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mockApi({page,responses}: {page: Page, responses?: Responses}) {
|
export async function mockApi({page,responses}: {page: Page, responses?: Responses}) {
|
||||||
@ -32,42 +40,66 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||||||
|
|
||||||
await mockApiResponse({
|
await mockApiResponse({
|
||||||
page,
|
page,
|
||||||
path: /\/ghost\/api\/admin\/settings\/\?group=.+/,
|
path: /\/ghost\/api\/admin\/settings\//,
|
||||||
method: 'GET',
|
responses: {
|
||||||
response: responses?.settings?.browse ?? responseFixtures.settings,
|
GET: {body: responses?.settings?.browse ?? responseFixtures.settings},
|
||||||
|
PUT: {body: responses?.settings?.edit ?? responseFixtures.settings}
|
||||||
|
},
|
||||||
lastApiRequest
|
lastApiRequest
|
||||||
});
|
});
|
||||||
|
|
||||||
await mockApiResponse({
|
await mockApiResponse({
|
||||||
page,
|
page,
|
||||||
path: /\/ghost\/api\/admin\/site\//,
|
path: /\/ghost\/api\/admin\/site\//,
|
||||||
method: 'GET',
|
responses: {
|
||||||
response: responses?.site?.browse ?? responseFixtures.site,
|
GET: {body: responses?.site?.browse ?? responseFixtures.site}
|
||||||
lastApiRequest
|
},
|
||||||
});
|
|
||||||
|
|
||||||
await mockApiResponse({
|
|
||||||
page,
|
|
||||||
path: /\/ghost\/api\/admin\/settings\/$/,
|
|
||||||
method: 'PUT',
|
|
||||||
response: responses?.settings?.edit ?? responseFixtures.settings,
|
|
||||||
lastApiRequest
|
lastApiRequest
|
||||||
});
|
});
|
||||||
|
|
||||||
await mockApiResponse({
|
await mockApiResponse({
|
||||||
page,
|
page,
|
||||||
path: /\/ghost\/api\/admin\/images\/upload\/$/,
|
path: /\/ghost\/api\/admin\/images\/upload\/$/,
|
||||||
method: 'POST',
|
responses: {
|
||||||
response: responses?.images?.upload ?? {images: [{url: 'http://example.com/image.png', ref: null}]},
|
POST: {body: responses?.images?.upload ?? {images: [{url: 'http://example.com/image.png', ref: null}]}}
|
||||||
|
},
|
||||||
lastApiRequest
|
lastApiRequest
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await mockApiResponse({
|
||||||
|
page,
|
||||||
|
path: /\/ghost\/api\/admin\/custom_theme_settings\/$/,
|
||||||
|
responses: {
|
||||||
|
GET: {body: responses?.custom_theme_settings?.browse ?? responseFixtures.custom_theme_settings},
|
||||||
|
PUT: {body: responses?.custom_theme_settings?.edit ?? responseFixtures.custom_theme_settings}
|
||||||
|
},
|
||||||
|
lastApiRequest
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route(responseFixtures.site.site.url, async (route) => {
|
||||||
|
if (!route.request().headers()['x-ghost-preview']) {
|
||||||
|
return route.continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
body: responses?.previewHtml?.homepage ?? '<html><head><style></style></head><body><div>test</div></body></html>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return lastApiRequest;
|
return lastApiRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockApiResponse({page, path, method, response, lastApiRequest}: { page: Page; path: string | RegExp; method: string; response: any; lastApiRequest: LastApiRequest }) {
|
interface MockResponse {
|
||||||
|
body: any
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockApiResponse({page, path, lastApiRequest, responses}: { page: Page; path: string | RegExp; lastApiRequest: LastApiRequest, responses: { [method: string]: MockResponse } }) {
|
||||||
await page.route(path, async (route) => {
|
await page.route(path, async (route) => {
|
||||||
if (route.request().method() !== method) {
|
const response = responses[route.request().method()];
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
return route.continue();
|
return route.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,8 +108,8 @@ async function mockApiResponse({page, path, method, response, lastApiRequest}: {
|
|||||||
lastApiRequest.url = route.request().url();
|
lastApiRequest.url = route.request().url();
|
||||||
|
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: response.status || 200,
|
||||||
body: JSON.stringify(response)
|
body: JSON.stringify(response.body)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"custom_theme_settings": [
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"default": "#ff0000",
|
||||||
|
"id": "648047658d265b0c8b33c58f",
|
||||||
|
"value": "#c73333",
|
||||||
|
"key": "navigation_color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"id": "648047658d265b0c8b33c590",
|
||||||
|
"value": null,
|
||||||
|
"key": "navigation_background_image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Logo on cover",
|
||||||
|
"Logo in the middle",
|
||||||
|
"Stacked"
|
||||||
|
],
|
||||||
|
"default": "Logo on cover",
|
||||||
|
"id": "648047658d265b0c8b33c591",
|
||||||
|
"value": "Stacked",
|
||||||
|
"key": "navigation_layout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"group": "homepage",
|
||||||
|
"id": "648047658d265b0c8b33c594",
|
||||||
|
"value": true,
|
||||||
|
"key": "show_publication_cover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"default": "Sign up for more like this.",
|
||||||
|
"group": "post",
|
||||||
|
"id": "648047658d265b0c8b33c599",
|
||||||
|
"value": "Sign up for more like this.",
|
||||||
|
"key": "email_signup_text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user