mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +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;
|
||||
|
||||
testId?: string;
|
||||
title?: string;
|
||||
okLabel?: string;
|
||||
okColor?: string;
|
||||
@ -32,6 +33,7 @@ export interface ModalProps {
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
size = 'md',
|
||||
testId,
|
||||
title,
|
||||
okLabel = 'OK',
|
||||
cancelLabel = 'Cancel',
|
||||
@ -179,7 +181,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
'pointer-events-none fixed inset-0 z-0',
|
||||
backDrop && 'bg-[rgba(98,109,121,0.15)] backdrop-blur-[3px]'
|
||||
)}></div>
|
||||
<section className={modalClasses} style={modalStyles}>
|
||||
<section className={modalClasses} data-testid={testId} style={modalStyles}>
|
||||
<div className={contentClasses}>
|
||||
<div className='h-full'>
|
||||
{title && <Heading level={4}>{title}</Heading>}
|
||||
@ -194,4 +196,4 @@ const Modal: React.FC<ModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
export default Modal;
|
||||
|
@ -10,6 +10,7 @@ import {IButton} from './Button';
|
||||
import {SelectOption} from './Select';
|
||||
|
||||
export interface PreviewModalProps {
|
||||
testId?: string;
|
||||
title?: string;
|
||||
sidebar?: React.ReactNode;
|
||||
preview?: React.ReactNode;
|
||||
@ -31,6 +32,7 @@ export interface PreviewModalProps {
|
||||
}
|
||||
|
||||
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
testId,
|
||||
title,
|
||||
sidebar,
|
||||
preview,
|
||||
@ -138,6 +140,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
customFooter={(<></>)}
|
||||
noPadding={true}
|
||||
size='full'
|
||||
testId={testId}
|
||||
title=''
|
||||
>
|
||||
<div className='flex h-full grow'>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type Tab = {
|
||||
id: string,
|
||||
@ -29,13 +30,25 @@ const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
|
||||
|
||||
return (
|
||||
<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 => (
|
||||
<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>
|
||||
{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>
|
||||
))}
|
||||
@ -43,4 +56,4 @@ const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TabView;
|
||||
export default TabView;
|
||||
|
@ -5,7 +5,7 @@ import {Setting, SiteData} from '../../types/api';
|
||||
// Define the Settings Context
|
||||
interface SettingsContextProps {
|
||||
settings: Setting[] | null;
|
||||
saveSettings: (updatedSettings: Setting[]) => Promise<void>;
|
||||
saveSettings: (updatedSettings: Setting[]) => Promise<Setting[]>;
|
||||
siteData: SiteData | null;
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ interface SettingsProviderProps {
|
||||
const SettingsContext = createContext<SettingsContextProps>({
|
||||
settings: null,
|
||||
siteData: null,
|
||||
saveSettings: async () => {}
|
||||
saveSettings: async () => []
|
||||
});
|
||||
|
||||
function serialiseSettingsData(settings: Setting[]): Setting[] {
|
||||
@ -100,16 +100,20 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
fetchSettings();
|
||||
}, [api]);
|
||||
|
||||
const saveSettings = useCallback(async (updatedSettings: Setting[]): Promise<void> => {
|
||||
const saveSettings = useCallback(async (updatedSettings: Setting[]) => {
|
||||
try {
|
||||
// handle transformation for settings before save
|
||||
updatedSettings = deserializeSettings(updatedSettings);
|
||||
// Make an API call to save the updated settings
|
||||
const data = await api.settings.edit(updatedSettings);
|
||||
const newSettings = serialiseSettingsData(data.settings);
|
||||
|
||||
setSettings(serialiseSettingsData(data.settings));
|
||||
setSettings(newSettings);
|
||||
|
||||
return newSettings;
|
||||
} catch (error) {
|
||||
// Log error in settings API
|
||||
return [];
|
||||
}
|
||||
}, [api]);
|
||||
|
||||
|
@ -60,7 +60,7 @@ const DesignModal: React.FC = () => {
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const {settings} = useContext(SettingsContext);
|
||||
const {settings, saveSettings} = useContext(SettingsContext);
|
||||
|
||||
const {
|
||||
formState,
|
||||
@ -80,8 +80,8 @@ const DesignModal: React.FC = () => {
|
||||
}
|
||||
|
||||
if (formState.settings.some(setting => setting.dirty)) {
|
||||
const response = await api.settings.edit(formState.settings);
|
||||
updateForm(state => ({...state, settings: response.settings}));
|
||||
const newSettings = await saveSettings(formState.settings.filter(setting => setting.dirty));
|
||||
updateForm(state => ({...state, settings: newSettings}));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -127,7 +127,7 @@ const DesignModal: React.FC = () => {
|
||||
return <PreviewModalContent
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
cancelLabel='Close'
|
||||
okLabel='Save'
|
||||
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving ...' : 'Save')}
|
||||
preview={
|
||||
<ThemePreview
|
||||
settings={{
|
||||
@ -148,6 +148,7 @@ const DesignModal: React.FC = () => {
|
||||
updateThemeSetting={updateThemeSetting}
|
||||
/>}
|
||||
sidebarPadding={false}
|
||||
testId='design-modal'
|
||||
title='Design'
|
||||
onCancel={() => {
|
||||
if (saveState === 'unsaved') {
|
||||
|
@ -14,6 +14,7 @@ const DesignSetting: React.FC = () => {
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||
description="Customize your site and manage themes"
|
||||
navid='branding-and-design'
|
||||
testId='design'
|
||||
title="Branding and design"
|
||||
/>
|
||||
);
|
||||
|
@ -115,11 +115,14 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => {
|
||||
return (
|
||||
<>
|
||||
<iframe
|
||||
ref={iframeRef} height="100%" title="Site Preview"
|
||||
ref={iframeRef}
|
||||
data-testid="theme-preview"
|
||||
height="100%"
|
||||
title="Site Preview"
|
||||
width="100%"
|
||||
></iframe>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemePreview;
|
||||
export default ThemePreview;
|
||||
|
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 = {
|
||||
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 {
|
||||
@ -22,6 +23,13 @@ interface Responses {
|
||||
images?: {
|
||||
upload?: any
|
||||
}
|
||||
custom_theme_settings?: {
|
||||
browse?: any
|
||||
edit?: any
|
||||
}
|
||||
previewHtml: {
|
||||
homepage?: string
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/settings\/\?group=.+/,
|
||||
method: 'GET',
|
||||
response: responses?.settings?.browse ?? responseFixtures.settings,
|
||||
path: /\/ghost\/api\/admin\/settings\//,
|
||||
responses: {
|
||||
GET: {body: responses?.settings?.browse ?? responseFixtures.settings},
|
||||
PUT: {body: responses?.settings?.edit ?? responseFixtures.settings}
|
||||
},
|
||||
lastApiRequest
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/site\//,
|
||||
method: 'GET',
|
||||
response: responses?.site?.browse ?? responseFixtures.site,
|
||||
lastApiRequest
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/settings\/$/,
|
||||
method: 'PUT',
|
||||
response: responses?.settings?.edit ?? responseFixtures.settings,
|
||||
responses: {
|
||||
GET: {body: responses?.site?.browse ?? responseFixtures.site}
|
||||
},
|
||||
lastApiRequest
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/images\/upload\/$/,
|
||||
method: 'POST',
|
||||
response: responses?.images?.upload ?? {images: [{url: 'http://example.com/image.png', ref: null}]},
|
||||
responses: {
|
||||
POST: {body: responses?.images?.upload ?? {images: [{url: 'http://example.com/image.png', ref: null}]}}
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (route.request().method() !== method) {
|
||||
const response = responses[route.request().method()];
|
||||
|
||||
if (!response) {
|
||||
return route.continue();
|
||||
}
|
||||
|
||||
@ -76,8 +108,8 @@ async function mockApiResponse({page, path, method, response, lastApiRequest}: {
|
||||
lastApiRequest.url = route.request().url();
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(response)
|
||||
status: response.status || 200,
|
||||
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