mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Added tests for more areas of AdminX settings (themes, design, multiselect) (#17134)
refs https://github.com/TryGhost/Team/issues/3349 Tidies up the remaining major pieces which were not covered by tests. Extends the existing test patterns, although the API mocks are getting a bit unmanageable.
This commit is contained in:
parent
12deff4a16
commit
768511c7cc
23 changed files with 1010 additions and 44 deletions
|
@ -7,6 +7,7 @@ export type ButtonSize = 'sm' | 'md';
|
|||
export interface ButtonProps {
|
||||
size?: ButtonSize;
|
||||
label?: React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
icon?: string;
|
||||
iconColorClass?: string;
|
||||
key?: string;
|
||||
|
@ -21,6 +22,7 @@ export interface ButtonProps {
|
|||
const Button: React.FC<ButtonProps> = ({
|
||||
size = 'md',
|
||||
label = '',
|
||||
hideLabel = false,
|
||||
icon = '',
|
||||
iconColorClass = 'text-black',
|
||||
color = 'clear',
|
||||
|
@ -75,9 +77,9 @@ const Button: React.FC<ButtonProps> = ({
|
|||
{...props}
|
||||
>
|
||||
{icon && <Icon colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}
|
||||
{label}
|
||||
{(label && hideLabel) ? <span className="sr-only">{label}</span> : label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
export default Button;
|
||||
|
|
|
@ -33,7 +33,7 @@ const Menu: React.FC<MenuProps> = ({trigger, triggerButtonProps, items, position
|
|||
};
|
||||
|
||||
if (!trigger) {
|
||||
trigger = <Button icon='ellipsis' {...triggerButtonProps} />;
|
||||
trigger = <Button icon='ellipsis' label='Menu' hideLabel {...triggerButtonProps} />;
|
||||
}
|
||||
|
||||
const menuClasses = clsx(
|
||||
|
@ -45,7 +45,7 @@ const Menu: React.FC<MenuProps> = ({trigger, triggerButtonProps, items, position
|
|||
|
||||
return (
|
||||
<div className={`relative inline-block ${className}`}>
|
||||
<div className={`fixed inset-0 z-40 ${menuOpen ? 'block' : 'hidden'}`} onClick={handleBackdropClick}></div>
|
||||
<div className={`fixed inset-0 z-40 ${menuOpen ? 'block' : 'hidden'}`} data-testid="menu-overlay" onClick={handleBackdropClick}></div>
|
||||
{/* Menu Trigger */}
|
||||
<div className='relative z-30' onClick={toggleMenu}>
|
||||
{trigger}
|
||||
|
@ -62,4 +62,4 @@ const Menu: React.FC<MenuProps> = ({trigger, triggerButtonProps, items, position
|
|||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
export default Menu;
|
||||
|
|
|
@ -10,12 +10,13 @@ interface DesktopChromeHeaderProps {
|
|||
toolbarClasses?: string;
|
||||
}
|
||||
|
||||
const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
||||
const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps & React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
size = 'md',
|
||||
toolbarLeft = '',
|
||||
toolbarCenter = '',
|
||||
toolbarRight = '',
|
||||
toolbarClasses = ''
|
||||
toolbarClasses = '',
|
||||
...props
|
||||
}) => {
|
||||
let containerSize;
|
||||
|
||||
|
@ -50,7 +51,7 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<header className={`relative flex items-center justify-center bg-grey-50 ${containerSize} ${toolbarClasses}`}>
|
||||
<header className={`relative flex items-center justify-center bg-grey-50 ${containerSize} ${toolbarClasses}`} {...props}>
|
||||
{toolbarLeft ?
|
||||
<div className='absolute left-5 flex h-full items-center'>
|
||||
{toolbarLeft}
|
||||
|
@ -74,4 +75,4 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default DesktopChromeHeader;
|
||||
export default DesktopChromeHeader;
|
||||
|
|
|
@ -4,9 +4,9 @@ interface MobileChromeProps {
|
|||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MobileChrome: React.FC<MobileChromeProps> = ({children}) => {
|
||||
const MobileChrome: React.FC<MobileChromeProps & React.HTMLAttributes<HTMLDivElement>> = ({children, ...props}) => {
|
||||
return (
|
||||
<div className='flex h-[775px] w-[380px] flex-col rounded-3xl bg-white p-2 shadow-xl'>
|
||||
<div className='flex h-[775px] w-[380px] flex-col rounded-3xl bg-white p-2 shadow-xl' {...props}>
|
||||
<div className='w-100 h-100 grow overflow-auto rounded-2xl border border-grey-100'>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -14,4 +14,4 @@ const MobileChrome: React.FC<MobileChromeProps> = ({children}) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default MobileChrome;
|
||||
export default MobileChrome;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
import React from 'react';
|
||||
import {GroupBase, MultiValue, OptionsOrGroups, default as ReactSelect, components} from 'react-select';
|
||||
import React, {useId, useMemo} from 'react';
|
||||
import {DropdownIndicatorProps, GroupBase, MultiValue, OptionProps, OptionsOrGroups, default as ReactSelect, components} from 'react-select';
|
||||
|
||||
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
|
||||
|
||||
|
@ -38,6 +38,18 @@ const multiValueColor = (color?: MultiSelectColor) => {
|
|||
}
|
||||
};
|
||||
|
||||
const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean}> = ({clearBg, ...props}) => (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] ${clearBg ? 'right-0' : 'right-4'} `}></div>
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
|
||||
const Option: React.FC<OptionProps<MultiSelectOption, true>> = ({children, ...optionProps}) => (
|
||||
<components.Option {...optionProps}>
|
||||
<span data-testid="multiselect-option">{children}</span>
|
||||
</components.Option>
|
||||
);
|
||||
|
||||
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
title = '',
|
||||
clearBg = false,
|
||||
|
@ -50,6 +62,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
const customClasses = {
|
||||
control: `w-full cursor-pointer appearance-none min-h-[40px] border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 outline-none ${error ? 'border-red' : 'border-grey-500 hover:border-grey-700'} ${(title && !clearBg) && 'mt-2'}`,
|
||||
valueContainer: 'gap-1',
|
||||
|
@ -61,15 +75,13 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
groupHeading: 'py-[6px] px-3 text-2xs font-semibold uppercase tracking-wide text-grey-700'
|
||||
};
|
||||
|
||||
const DropdownIndicator: React.FC<any> = ddiProps => (
|
||||
<components.DropdownIndicator {...ddiProps}>
|
||||
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] ${clearBg ? 'right-0' : 'right-4'} `}></div>
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
const dropdownIndicatorComponent = useMemo(() => {
|
||||
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} />;
|
||||
}, [clearBg]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
{title && <Heading useLabelTag={true} grey>{title}</Heading>}
|
||||
{title && <Heading htmlFor={id} grey useLabelTag>{title}</Heading>}
|
||||
<ReactSelect
|
||||
classNames={{
|
||||
menuList: () => 'z-50',
|
||||
|
@ -83,7 +95,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
groupHeading: () => customClasses.groupHeading
|
||||
}}
|
||||
closeMenuOnSelect={false}
|
||||
components={{DropdownIndicator}}
|
||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option}}
|
||||
inputId={id}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
placeholder={placeholder ? placeholder : ''}
|
||||
|
|
|
@ -66,7 +66,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
|
||||
if (view === 'mobile') {
|
||||
preview = (
|
||||
<MobileChrome>
|
||||
<MobileChrome data-testid="preview-mobile">
|
||||
{preview}
|
||||
</MobileChrome>
|
||||
);
|
||||
|
@ -94,6 +94,8 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
buttons={[
|
||||
{
|
||||
icon: 'laptop',
|
||||
label: 'Desktop',
|
||||
hideLabel: true,
|
||||
link: true,
|
||||
size: 'sm',
|
||||
iconColorClass: (view === 'desktop' ? 'text-black' : unSelectedIconColorClass),
|
||||
|
@ -103,6 +105,8 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
},
|
||||
{
|
||||
icon: 'mobile',
|
||||
label: 'Mobile',
|
||||
hideLabel: true,
|
||||
link: true,
|
||||
size: 'sm',
|
||||
iconColorClass: (view === 'mobile' ? 'text-black' : unSelectedIconColorClass),
|
||||
|
@ -117,6 +121,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
preview = (
|
||||
<>
|
||||
<DesktopChromeHeader
|
||||
data-testid="design-toolbar"
|
||||
size='lg'
|
||||
toolbarCenter={<></>}
|
||||
toolbarLeft={toolbarLeft}
|
||||
|
|
|
@ -16,7 +16,7 @@ const Sidebar: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div className="hidden md:!visible md:!block md:h-[calc(100vh-5vmin-84px)] md:w-[300px] md:overflow-y-scroll md:pt-[32px]">
|
||||
<TextField containerClassName="mb-10" placeholder="Search" value={filter} onChange={e => setFilter(e.target.value)} />
|
||||
<TextField containerClassName="mb-10" placeholder="Search" title="Search" value={filter} hideTitle onChange={e => setFilter(e.target.value)} />
|
||||
|
||||
<SettingNavSection title="General">
|
||||
<SettingNavItem navid='title-and-description' title="Title and description" onClick={handleSectionClick} />
|
||||
|
|
|
@ -47,7 +47,7 @@ const Sidebar: React.FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className='p-7'>
|
||||
<div className='p-7' data-testid="design-setting-tabs">
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -214,6 +214,7 @@ const ChangeThemeModal = NiceModal.create(() => {
|
|||
noPadding={true}
|
||||
scrolling={currentTab === 'official' ? false : true}
|
||||
size='full'
|
||||
testId='theme-modal'
|
||||
title=''
|
||||
>
|
||||
<div className='flex h-full justify-between'>
|
||||
|
|
|
@ -171,6 +171,7 @@ const ThemeList:React.FC<ThemeSettingProps> = ({
|
|||
detail={detail}
|
||||
id={`theme-${theme.name}`}
|
||||
separator={false}
|
||||
testId='theme-list-item'
|
||||
title={label}
|
||||
/>
|
||||
);
|
||||
|
@ -193,4 +194,4 @@ const AdvancedThemeSettings: React.FC<ThemeSettingProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default AdvancedThemeSettings;
|
||||
export default AdvancedThemeSettings;
|
||||
|
|
|
@ -18,13 +18,13 @@ const OfficialThemes: React.FC<{
|
|||
<div className='mt-[6vmin] grid grid-cols-1 gap-[6vmin] sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
|
||||
{officialThemes.map((theme) => {
|
||||
return (
|
||||
<div key={theme.name} className='flex cursor-pointer flex-col gap-3' onClick={() => {
|
||||
<button key={theme.name} className='flex cursor-pointer flex-col gap-3 text-left' type='button' onClick={() => {
|
||||
onSelectTheme?.(theme);
|
||||
}}>
|
||||
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
|
||||
<div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
|
||||
<img
|
||||
alt="Headline Theme"
|
||||
alt={`${theme.name} Theme`}
|
||||
className='h-full w-full object-contain'
|
||||
src={`${adminRoot}${theme.image}`}
|
||||
/>
|
||||
|
@ -33,7 +33,7 @@ const OfficialThemes: React.FC<{
|
|||
<Heading level={4}>{theme.name}</Heading>
|
||||
<span className='text-sm text-grey-700'>{theme.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -41,4 +41,4 @@ const OfficialThemes: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
export default OfficialThemes;
|
||||
export default OfficialThemes;
|
||||
|
|
|
@ -7,7 +7,27 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|||
<React.StrictMode>
|
||||
<App
|
||||
ghostVersion='5.x'
|
||||
officialThemes={[]}
|
||||
officialThemes={[{
|
||||
name: 'Casper',
|
||||
category: 'Blog',
|
||||
previewUrl: 'https://demo.ghost.io/',
|
||||
ref: 'default',
|
||||
image: 'assets/img/themes/Casper.png'
|
||||
}, {
|
||||
name: 'Headline',
|
||||
category: 'News',
|
||||
url: 'https://github.com/TryGhost/Headline',
|
||||
previewUrl: 'https://headline.ghost.io',
|
||||
ref: 'TryGhost/Headline',
|
||||
image: 'assets/img/themes/Headline.png'
|
||||
}, {
|
||||
name: 'Edition',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Edition',
|
||||
previewUrl: 'https://edition.ghost.io/',
|
||||
ref: 'TryGhost/Edition',
|
||||
image: 'assets/img/themes/Edition.png'
|
||||
}]}
|
||||
/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -55,4 +55,51 @@ test.describe('Default recipient settings', async () => {
|
|||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports selecting specific tiers, labels and offers', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
settings: {
|
||||
edit: updatedSettingsResponse([
|
||||
{
|
||||
key: 'editor_default_email_recipients',
|
||||
value: 'filter'
|
||||
},
|
||||
{
|
||||
key: 'editor_default_email_recipients_filter',
|
||||
value: '645453f4d254799990dd0e22,label:first-label,offer_redemptions:6487ea6464fca78ec2fff5fe'
|
||||
}
|
||||
])
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('default-recipients');
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Default newsletter recipients').selectOption({label: 'Specific people'});
|
||||
await section.getByLabel('Select tiers').click();
|
||||
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'first-label'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'First offer'}).click();
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(section.getByText('Specific people')).toHaveCount(1);
|
||||
|
||||
expect(lastApiRequests.settings.edit.body).toEqual({
|
||||
settings: [
|
||||
{
|
||||
key: 'editor_default_email_recipients',
|
||||
value: 'filter'
|
||||
},
|
||||
{
|
||||
key: 'editor_default_email_recipients_filter',
|
||||
value: '645453f4d254799990dd0e22,label:first-label,offer_redemptions:6487ea6464fca78ec2fff5fe'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, updatedSettingsResponse} from '../../utils/e2e';
|
||||
import {mockApi, responseFixtures, updatedSettingsResponse} from '../../utils/e2e';
|
||||
|
||||
test.describe('Access settings', async () => {
|
||||
test('Supports editing access', async ({page}) => {
|
||||
|
@ -43,4 +43,38 @@ test.describe('Access settings', async () => {
|
|||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports selecting specific tiers', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
settings: {
|
||||
edit: updatedSettingsResponse([
|
||||
{key: 'default_content_visibility', value: 'tiers'},
|
||||
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.map(tier => tier.id))}
|
||||
])
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('access');
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Default post access').selectOption({label: 'Specific tiers'});
|
||||
await section.getByLabel('Select tiers').click();
|
||||
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Ultimate Starlight Diamond Supporter'}).click();
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(section.getByText('Specific tiers')).toHaveCount(1);
|
||||
|
||||
expect(lastApiRequests.settings.edit.body).toEqual({
|
||||
settings: [
|
||||
{key: 'default_content_visibility', value: 'tiers'},
|
||||
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.map(tier => tier.id))}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
22
apps/admin-x-settings/test/e2e/search.test.ts
Normal file
22
apps/admin-x-settings/test/e2e/search.test.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi} from '../utils/e2e';
|
||||
|
||||
test.describe('Search', async () => {
|
||||
test('Hiding and showing groups based on the search term', async ({page}) => {
|
||||
await mockApi({page});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const searchBar = page.getByLabel('Search');
|
||||
|
||||
await searchBar.fill('theme');
|
||||
|
||||
await expect(page.getByTestId('theme')).toBeVisible();
|
||||
await expect(page.getByTestId('title-and-description')).not.toBeVisible();
|
||||
|
||||
await searchBar.fill('title');
|
||||
|
||||
await expect(page.getByTestId('theme')).not.toBeVisible();
|
||||
await expect(page.getByTestId('title-and-description')).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -2,6 +2,51 @@ import {expect, test} from '@playwright/test';
|
|||
import {mockApi} from '../../utils/e2e';
|
||||
|
||||
test.describe('Design settings', async () => {
|
||||
test('Working with the preview', async ({page}) => {
|
||||
await mockApi({page, responses: {
|
||||
previewHtml: {
|
||||
homepage: '<html><head><style></style></head><body><div>homepage preview</div></body></html>',
|
||||
post: '<html><head><style></style></head><body><div>post 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');
|
||||
|
||||
// Homepage and post preview
|
||||
|
||||
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
|
||||
|
||||
await modal.getByTestId('design-toolbar').getByRole('tab', {name: 'Post'}).click();
|
||||
|
||||
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('post preview')).toHaveCount(1);
|
||||
|
||||
// Desktop and mobile preview
|
||||
|
||||
await modal.getByRole('button', {name: 'Mobile'}).click();
|
||||
|
||||
await expect(modal.getByTestId('preview-mobile')).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', {name: 'Desktop'}).click();
|
||||
|
||||
await expect(modal.getByTestId('preview-mobile')).not.toBeVisible();
|
||||
|
||||
// Switching preview based on settings tab
|
||||
|
||||
await modal.getByTestId('design-setting-tabs').getByRole('tab', {name: 'Homepage'}).click();
|
||||
|
||||
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
|
||||
|
||||
await modal.getByTestId('design-setting-tabs').getByRole('tab', {name: 'Post'}).click();
|
||||
|
||||
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('post preview')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Editing brand settings', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
previewHtml: {
|
||||
|
@ -35,7 +80,7 @@ test.describe('Design settings', async () => {
|
|||
|
||||
test('Editing custom theme settings', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
custom_theme_settings: {
|
||||
customThemeSettings: {
|
||||
browse: {
|
||||
custom_theme_settings: [{
|
||||
type: 'select',
|
||||
|
@ -72,7 +117,7 @@ test.describe('Design settings', async () => {
|
|||
const expectedEncoded = new URLSearchParams([['custom', JSON.stringify(expectedSettings)]]).toString();
|
||||
expect(lastApiRequests.previewHtml.homepage.headers?.['x-ghost-preview']).toMatch(new RegExp(`&${expectedEncoded.replace(/\+/g, '\\+')}`));
|
||||
|
||||
expect(lastApiRequests.custom_theme_settings.edit.body).toMatchObject({
|
||||
expect(lastApiRequests.customThemeSettings.edit.body).toMatchObject({
|
||||
custom_theme_settings: [
|
||||
{key: 'navigation_layout', value: 'Logo in the middle'}
|
||||
]
|
||||
|
|
122
apps/admin-x-settings/test/e2e/site/theme.test.ts
Normal file
122
apps/admin-x-settings/test/e2e/site/theme.test.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../utils/e2e';
|
||||
|
||||
test.describe('Theme settings', async () => {
|
||||
test('Browsing and installing default themes', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
themes: {
|
||||
install: {
|
||||
themes: [{
|
||||
name: 'headline',
|
||||
package: {},
|
||||
active: false,
|
||||
templates: []
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('theme');
|
||||
|
||||
await section.getByRole('button', {name: 'Manage themes'}).click();
|
||||
|
||||
const modal = page.getByTestId('theme-modal');
|
||||
|
||||
// The default theme is always considered "installed"
|
||||
|
||||
await modal.getByRole('button', {name: /Casper/}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Installed'})).toBeVisible();
|
||||
await expect(modal.getByRole('button', {name: 'Installed'})).toBeDisabled();
|
||||
|
||||
await expect(page.locator('iframe[title="Theme preview"]')).toHaveAttribute('src', 'https://demo.ghost.io/');
|
||||
|
||||
await modal.getByRole('button', {name: 'Official themes'}).click();
|
||||
|
||||
// The "edition" theme is activated in fixtures
|
||||
|
||||
await modal.getByRole('button', {name: /Edition/}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Activated'})).toBeVisible();
|
||||
await expect(modal.getByRole('button', {name: 'Activated'})).toBeDisabled();
|
||||
|
||||
await expect(page.locator('iframe[title="Theme preview"]')).toHaveAttribute('src', 'https://edition.ghost.io/');
|
||||
|
||||
await modal.getByRole('button', {name: 'Official themes'}).click();
|
||||
|
||||
// Try installing another theme
|
||||
|
||||
await modal.getByRole('button', {name: /Headline/}).click();
|
||||
|
||||
await modal.getByRole('button', {name: 'Install Headline'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Installed'})).toBeVisible();
|
||||
await expect(modal.getByRole('button', {name: 'Installed'})).toBeDisabled();
|
||||
await expect(page.getByTestId('toast')).toHaveText(/Theme installed - headline/);
|
||||
|
||||
expect(lastApiRequests.themes.install.url).toMatch(/\?source=github&ref=TryGhost%2FHeadline/);
|
||||
});
|
||||
|
||||
test('Managing installed themes', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
themes: {
|
||||
activate: {
|
||||
themes: [{
|
||||
...responseFixtures.themes.themes.find(theme => theme.name === 'casper')!,
|
||||
active: true
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('theme');
|
||||
|
||||
await section.getByRole('button', {name: 'Manage themes'}).click();
|
||||
|
||||
const modal = page.getByTestId('theme-modal');
|
||||
|
||||
await modal.getByRole('tab', {name: 'Installed'}).click();
|
||||
|
||||
await expect(modal.getByTestId('theme-list-item')).toHaveCount(2);
|
||||
|
||||
const casper = modal.getByTestId('theme-list-item').filter({hasText: /casper/});
|
||||
const edition = modal.getByTestId('theme-list-item').filter({hasText: /edition/});
|
||||
|
||||
// Activate the inactive theme
|
||||
|
||||
await expect(casper.getByRole('button', {name: 'Activate'})).toBeVisible();
|
||||
await expect(edition).toHaveText(/Active/);
|
||||
|
||||
await casper.getByRole('button', {name: 'Activate'}).click();
|
||||
|
||||
await expect(casper).toHaveText(/Active/);
|
||||
await expect(edition.getByRole('button', {name: 'Activate'})).toBeVisible();
|
||||
|
||||
expect(lastApiRequests.themes.activate.url).toMatch(/\/themes\/casper\/activate\//);
|
||||
|
||||
// Download the active theme
|
||||
|
||||
await casper.getByRole('button', {name: 'Menu'}).click();
|
||||
await casper.getByRole('button', {name: 'Download'}).click();
|
||||
|
||||
await expect(page.locator('iframe#iframeDownload')).toHaveAttribute('src', /\/api\/admin\/themes\/casper\/download/);
|
||||
|
||||
await page.locator('[data-testid="menu-overlay"]:visible').click();
|
||||
|
||||
// Delete the inactive theme
|
||||
|
||||
await edition.getByRole('button', {name: 'Menu'}).click();
|
||||
await edition.getByRole('button', {name: 'Delete'}).click();
|
||||
|
||||
const confirmation = page.getByTestId('confirmation-modal');
|
||||
await confirmation.getByRole('button', {name: 'Delete'}).click();
|
||||
|
||||
await expect(modal.getByTestId('theme-list-item')).toHaveCount(1);
|
||||
|
||||
expect(lastApiRequests.themes.delete.url).toMatch(/\/themes\/edition\/$/);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import {CustomThemeSettingsResponseType, ImagesResponseType, InvitesResponseType, RolesResponseType, SettingsResponseType, SiteResponseType, UsersResponseType} from '../../src/utils/api';
|
||||
import {CustomThemeSettingsResponseType, ImagesResponseType, InvitesResponseType, LabelsResponseType, OffersResponseType, PostsResponseType, RolesResponseType, SettingsResponseType, SiteResponseType, ThemesResponseType, TiersResponseType, UsersResponseType} from '../../src/utils/api';
|
||||
import {Page, Request} from '@playwright/test';
|
||||
import {readFileSync} from 'fs';
|
||||
|
||||
|
@ -9,7 +9,11 @@ export const responseFixtures = {
|
|||
roles: JSON.parse(readFileSync(`${__dirname}/responses/roles.json`).toString()) as RolesResponseType,
|
||||
site: JSON.parse(readFileSync(`${__dirname}/responses/site.json`).toString()) as SiteResponseType,
|
||||
invites: JSON.parse(readFileSync(`${__dirname}/responses/invites.json`).toString()) as InvitesResponseType,
|
||||
custom_theme_settings: JSON.parse(readFileSync(`${__dirname}/responses/custom_theme_settings.json`).toString()) as CustomThemeSettingsResponseType
|
||||
customThemeSettings: JSON.parse(readFileSync(`${__dirname}/responses/custom_theme_settings.json`).toString()) as CustomThemeSettingsResponseType,
|
||||
tiers: JSON.parse(readFileSync(`${__dirname}/responses/tiers.json`).toString()) as TiersResponseType,
|
||||
labels: JSON.parse(readFileSync(`${__dirname}/responses/labels.json`).toString()) as LabelsResponseType,
|
||||
offers: JSON.parse(readFileSync(`${__dirname}/responses/offers.json`).toString()) as OffersResponseType,
|
||||
themes: JSON.parse(readFileSync(`${__dirname}/responses/themes.json`).toString()) as ThemesResponseType
|
||||
};
|
||||
|
||||
interface Responses {
|
||||
|
@ -39,12 +43,32 @@ interface Responses {
|
|||
images?: {
|
||||
upload?: ImagesResponseType
|
||||
}
|
||||
custom_theme_settings?: {
|
||||
customThemeSettings?: {
|
||||
browse?: CustomThemeSettingsResponseType
|
||||
edit?: CustomThemeSettingsResponseType
|
||||
}
|
||||
latestPost?: {
|
||||
browse?: PostsResponseType
|
||||
}
|
||||
tiers?: {
|
||||
browse?: TiersResponseType
|
||||
}
|
||||
labels?: {
|
||||
browse?: LabelsResponseType
|
||||
}
|
||||
offers?: {
|
||||
browse?: OffersResponseType
|
||||
}
|
||||
themes?: {
|
||||
browse?: ThemesResponseType
|
||||
activate?: ThemesResponseType
|
||||
delete?: ThemesResponseType
|
||||
install?: ThemesResponseType
|
||||
upload?: ThemesResponseType
|
||||
}
|
||||
previewHtml?: {
|
||||
homepage?: string
|
||||
post?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,12 +105,32 @@ type LastRequests = {
|
|||
images: {
|
||||
upload: RequestRecord
|
||||
}
|
||||
custom_theme_settings: {
|
||||
customThemeSettings: {
|
||||
browse: RequestRecord
|
||||
edit: RequestRecord
|
||||
}
|
||||
latestPost: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
tiers: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
labels: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
offers: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
themes: {
|
||||
browse: RequestRecord
|
||||
activate: RequestRecord
|
||||
delete: RequestRecord
|
||||
install: RequestRecord
|
||||
upload: RequestRecord
|
||||
}
|
||||
previewHtml: {
|
||||
homepage: RequestRecord
|
||||
post: RequestRecord
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -98,8 +142,13 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
invites: {browse: {}, add: {}, delete: {}},
|
||||
site: {browse: {}},
|
||||
images: {upload: {}},
|
||||
custom_theme_settings: {browse: {}, edit: {}},
|
||||
previewHtml: {homepage: {}}
|
||||
customThemeSettings: {browse: {}, edit: {}},
|
||||
latestPost: {browse: {}},
|
||||
tiers: {browse: {}},
|
||||
labels: {browse: {}},
|
||||
offers: {browse: {}},
|
||||
themes: {browse: {}, activate: {}, delete: {}, install: {}, upload: {}},
|
||||
previewHtml: {homepage: {}, post: {}}
|
||||
};
|
||||
|
||||
await mockApiResponse({
|
||||
|
@ -235,17 +284,116 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/themes\/$/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.themes?.browse ?? responseFixtures.themes,
|
||||
updateLastRequest: lastApiRequests.themes.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/themes\/(casper|edition|headline)\/$/,
|
||||
respondTo: {
|
||||
DELETE: {
|
||||
body: responses?.themes?.delete ?? responseFixtures.themes,
|
||||
updateLastRequest: lastApiRequests.themes.delete
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/themes\/\w+\/activate\/$/,
|
||||
respondTo: {
|
||||
PUT: {
|
||||
body: responses?.themes?.activate ?? responseFixtures.themes,
|
||||
updateLastRequest: lastApiRequests.themes.activate
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/themes\/install\//,
|
||||
respondTo: {
|
||||
POST: {
|
||||
body: responses?.themes?.install ?? responseFixtures.themes,
|
||||
updateLastRequest: lastApiRequests.themes.install
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/themes\/upload\/$/,
|
||||
respondTo: {
|
||||
POST: {
|
||||
body: responses?.themes?.upload ?? responseFixtures.themes,
|
||||
updateLastRequest: lastApiRequests.themes.upload
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/custom_theme_settings\/$/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.custom_theme_settings?.browse ?? responseFixtures.custom_theme_settings,
|
||||
updateLastRequest: lastApiRequests.custom_theme_settings.browse
|
||||
body: responses?.customThemeSettings?.browse ?? responseFixtures.customThemeSettings,
|
||||
updateLastRequest: lastApiRequests.customThemeSettings.browse
|
||||
},
|
||||
PUT: {
|
||||
body: responses?.custom_theme_settings?.edit ?? responseFixtures.custom_theme_settings,
|
||||
updateLastRequest: lastApiRequests.custom_theme_settings.edit
|
||||
body: responses?.customThemeSettings?.edit ?? responseFixtures.customThemeSettings,
|
||||
updateLastRequest: lastApiRequests.customThemeSettings.edit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/posts\/\?filter=/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.latestPost?.browse ?? {posts: [{id: '1', url: `${responseFixtures.site.site.url}/test-post/`}]},
|
||||
updateLastRequest: lastApiRequests.latestPost.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/tiers\/\?filter=/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.tiers?.browse ?? responseFixtures.tiers,
|
||||
updateLastRequest: lastApiRequests.tiers.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/labels\/\?limit=all$/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.labels?.browse ?? responseFixtures.labels,
|
||||
updateLastRequest: lastApiRequests.labels.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/offers\/\?limit=all$/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.offers?.browse ?? responseFixtures.offers,
|
||||
updateLastRequest: lastApiRequests.offers.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -256,12 +404,24 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
respondTo: {
|
||||
POST: {
|
||||
condition: request => !!request.headers()['x-ghost-preview'],
|
||||
body: responses?.previewHtml?.homepage ?? '<html><head><style></style></head><body><div>test</div></body></html>',
|
||||
body: responses?.previewHtml?.homepage ?? '<html><head><style></style></head><body><div>homepage</div></body></html>',
|
||||
updateLastRequest: lastApiRequests.previewHtml.homepage
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: `${responseFixtures.site.site.url}/test-post/`,
|
||||
respondTo: {
|
||||
POST: {
|
||||
condition: request => !!request.headers()['x-ghost-preview'],
|
||||
body: responses?.previewHtml?.post ?? '<html><head><style></style></head><body><div>post</div></body></html>',
|
||||
updateLastRequest: lastApiRequests.previewHtml.post
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return lastApiRequests;
|
||||
}
|
||||
|
||||
|
|
35
apps/admin-x-settings/test/utils/responses/labels.json
Normal file
35
apps/admin-x-settings/test/utils/responses/labels.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"labels": [
|
||||
{
|
||||
"id": "645c4c8564f46d9f26eb93ae",
|
||||
"name": "first-label",
|
||||
"slug": "first-label",
|
||||
"created_at": "2023-05-11T02:01:41.000Z",
|
||||
"updated_at": "2023-05-11T02:01:41.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645c4c6764f46d9f26eb93a9",
|
||||
"name": "second-label",
|
||||
"slug": "second-label",
|
||||
"created_at": "2023-05-11T02:01:11.000Z",
|
||||
"updated_at": "2023-05-11T02:01:11.000Z"
|
||||
},
|
||||
{
|
||||
"id": "645c4c6764f46d9f26eb93a8",
|
||||
"name": "third-label",
|
||||
"slug": "third-label",
|
||||
"created_at": "2023-05-11T02:01:11.000Z",
|
||||
"updated_at": "2023-05-11T02:01:11.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": "all",
|
||||
"pages": 1,
|
||||
"total": 3,
|
||||
"next": null,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}
|
44
apps/admin-x-settings/test/utils/responses/offers.json
Normal file
44
apps/admin-x-settings/test/utils/responses/offers.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"offers": [
|
||||
{
|
||||
"id": "6487ea6464fca78ec2fff5fe",
|
||||
"name": "First offer",
|
||||
"code": "first-offer",
|
||||
"display_title": "First offer",
|
||||
"display_description": "",
|
||||
"type": "percent",
|
||||
"cadence": "month",
|
||||
"amount": 10,
|
||||
"duration": "once",
|
||||
"duration_in_months": null,
|
||||
"currency_restriction": false,
|
||||
"currency": null,
|
||||
"status": "active",
|
||||
"redemption_count": 0,
|
||||
"tier": {
|
||||
"id": "645453f4d254799990dd0e22",
|
||||
"name": "Supporter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6487f0c364fca78ec2fff600",
|
||||
"name": "Second offer",
|
||||
"code": "second-offer",
|
||||
"display_title": "Second offer",
|
||||
"display_description": "",
|
||||
"type": "percent",
|
||||
"cadence": "month",
|
||||
"amount": 12,
|
||||
"duration": "repeating",
|
||||
"duration_in_months": 3,
|
||||
"currency_restriction": false,
|
||||
"currency": null,
|
||||
"status": "active",
|
||||
"redemption_count": 0,
|
||||
"tier": {
|
||||
"id": "645453f4d254799990dd0e22",
|
||||
"name": "Supporter"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
BIN
apps/admin-x-settings/test/utils/responses/theme.zip
Normal file
BIN
apps/admin-x-settings/test/utils/responses/theme.zip
Normal file
Binary file not shown.
362
apps/admin-x-settings/test/utils/responses/themes.json
Normal file
362
apps/admin-x-settings/test/utils/responses/themes.json
Normal file
|
@ -0,0 +1,362 @@
|
|||
{
|
||||
"themes": [
|
||||
{
|
||||
"name": "casper",
|
||||
"package": {
|
||||
"name": "casper",
|
||||
"description": "A clean, minimal default theme for the Ghost publishing platform",
|
||||
"demo": "https://demo.ghost.io",
|
||||
"version": "5.4.10",
|
||||
"engines": {
|
||||
"ghost": ">=5.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"screenshots": {
|
||||
"desktop": "assets/screenshot-desktop.jpg",
|
||||
"mobile": "assets/screenshot-mobile.jpg"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "gulp",
|
||||
"zip": "gulp zip",
|
||||
"test": "gscan .",
|
||||
"test:ci": "gscan --fatal --verbose .",
|
||||
"pretest": "gulp build",
|
||||
"preship": "yarn test",
|
||||
"ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn version && git push --follow-tags; else echo \"Uncomitted changes found.\" && exit 1; fi",
|
||||
"postship": "git fetch && gulp release"
|
||||
},
|
||||
"author": {
|
||||
"name": "Ghost Foundation",
|
||||
"email": "hello@ghost.org",
|
||||
"url": "https://ghost.org/"
|
||||
},
|
||||
"gpm": {
|
||||
"type": "theme",
|
||||
"categories": [
|
||||
"Minimal",
|
||||
"Magazine"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"ghost",
|
||||
"theme",
|
||||
"ghost-theme"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TryGhost/Casper.git"
|
||||
},
|
||||
"bugs": "https://github.com/TryGhost/Casper/issues",
|
||||
"contributors": "https://github.com/TryGhost/Casper/graphs/contributors",
|
||||
"devDependencies": {
|
||||
"@tryghost/release-utils": "0.8.1",
|
||||
"autoprefixer": "10.4.7",
|
||||
"beeper": "2.1.0",
|
||||
"cssnano": "5.1.12",
|
||||
"gscan": "4.36.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-livereload": "4.0.2",
|
||||
"gulp-postcss": "9.0.1",
|
||||
"gulp-uglify": "3.0.2",
|
||||
"gulp-zip": "5.1.0",
|
||||
"inquirer": "8.2.4",
|
||||
"postcss": "8.2.13",
|
||||
"postcss-color-mod-function": "3.0.3",
|
||||
"postcss-easy-import": "4.0.0",
|
||||
"pump": "3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"config": {
|
||||
"posts_per_page": 25,
|
||||
"image_sizes": {
|
||||
"xxs": {
|
||||
"width": 30
|
||||
},
|
||||
"xs": {
|
||||
"width": 100
|
||||
},
|
||||
"s": {
|
||||
"width": 300
|
||||
},
|
||||
"m": {
|
||||
"width": 600
|
||||
},
|
||||
"l": {
|
||||
"width": 1000
|
||||
},
|
||||
"xl": {
|
||||
"width": 2000
|
||||
}
|
||||
},
|
||||
"card_assets": true,
|
||||
"custom": {
|
||||
"navigation_layout": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Logo on cover",
|
||||
"Logo in the middle",
|
||||
"Stacked"
|
||||
],
|
||||
"default": "Logo on cover"
|
||||
},
|
||||
"title_font": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Modern sans-serif",
|
||||
"Elegant serif"
|
||||
],
|
||||
"default": "Modern sans-serif"
|
||||
},
|
||||
"body_font": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Modern sans-serif",
|
||||
"Elegant serif"
|
||||
],
|
||||
"default": "Elegant serif"
|
||||
},
|
||||
"show_publication_cover": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"group": "homepage"
|
||||
},
|
||||
"header_style": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Center aligned",
|
||||
"Left aligned",
|
||||
"Hidden"
|
||||
],
|
||||
"default": "Center aligned",
|
||||
"group": "homepage"
|
||||
},
|
||||
"feed_layout": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Classic",
|
||||
"Grid",
|
||||
"List"
|
||||
],
|
||||
"default": "Classic",
|
||||
"group": "homepage"
|
||||
},
|
||||
"color_scheme": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Light",
|
||||
"Dark",
|
||||
"Auto"
|
||||
],
|
||||
"default": "Light"
|
||||
},
|
||||
"post_image_style": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Wide",
|
||||
"Full",
|
||||
"Small",
|
||||
"Hidden"
|
||||
],
|
||||
"default": "Wide",
|
||||
"group": "post"
|
||||
},
|
||||
"email_signup_text": {
|
||||
"type": "text",
|
||||
"default": "Sign up for more like this.",
|
||||
"group": "post"
|
||||
},
|
||||
"show_recent_posts_footer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"group": "post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"renovate": {
|
||||
"extends": [
|
||||
"@tryghost:theme"
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"name": "edition",
|
||||
"package": {
|
||||
"name": "edition",
|
||||
"description": "A clean, minimal newsletter theme for the Ghost publishing platform",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"ghost": ">=5.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Ghost Foundation",
|
||||
"email": "hello@ghost.org",
|
||||
"url": "https://ghost.org"
|
||||
},
|
||||
"keywords": [
|
||||
"ghost",
|
||||
"theme",
|
||||
"ghost-theme"
|
||||
],
|
||||
"docs": "https://edition.ghost.io/about/",
|
||||
"config": {
|
||||
"posts_per_page": 10,
|
||||
"image_sizes": {
|
||||
"xs": {
|
||||
"width": 150
|
||||
},
|
||||
"s": {
|
||||
"width": 400
|
||||
},
|
||||
"m": {
|
||||
"width": 750
|
||||
},
|
||||
"l": {
|
||||
"width": 960
|
||||
},
|
||||
"xl": {
|
||||
"width": 1140
|
||||
},
|
||||
"xxl": {
|
||||
"width": 1920
|
||||
}
|
||||
},
|
||||
"card_assets": true,
|
||||
"custom": {
|
||||
"navigation_layout": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Logo on the left",
|
||||
"Logo in the middle",
|
||||
"Stacked"
|
||||
],
|
||||
"default": "Logo on the left"
|
||||
},
|
||||
"title_font": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Modern sans-serif",
|
||||
"Elegant serif"
|
||||
],
|
||||
"default": "Modern sans-serif"
|
||||
},
|
||||
"body_font": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Modern sans-serif",
|
||||
"Elegant serif"
|
||||
],
|
||||
"default": "Modern sans-serif"
|
||||
},
|
||||
"email_signup_text": {
|
||||
"type": "text",
|
||||
"group": "homepage"
|
||||
},
|
||||
"publication_cover_style": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Fullscreen",
|
||||
"Half screen"
|
||||
],
|
||||
"default": "Fullscreen",
|
||||
"group": "homepage"
|
||||
},
|
||||
"show_featured_posts": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"group": "homepage"
|
||||
},
|
||||
"featured_title": {
|
||||
"type": "text",
|
||||
"default": "Featured articles",
|
||||
"group": "homepage"
|
||||
},
|
||||
"feed_title": {
|
||||
"type": "text",
|
||||
"default": "Latest",
|
||||
"group": "homepage"
|
||||
},
|
||||
"feed_layout": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Expanded",
|
||||
"Right thumbnail",
|
||||
"Text-only",
|
||||
"Minimal"
|
||||
],
|
||||
"default": "Expanded",
|
||||
"group": "homepage"
|
||||
},
|
||||
"show_author": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"group": "post"
|
||||
},
|
||||
"show_related_posts": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"group": "post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "gulp",
|
||||
"test": "gscan .",
|
||||
"zip": "gulp zip"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tryghost/shared-theme-assets": "2.2.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"beeper": "2.1.0",
|
||||
"cssnano": "6.0.1",
|
||||
"gscan": "4.36.3",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-livereload": "4.0.2",
|
||||
"gulp-postcss": "9.0.1",
|
||||
"gulp-uglify": "3.0.2",
|
||||
"gulp-zip": "5.1.0",
|
||||
"postcss": "8.4.24",
|
||||
"postcss-easy-import": "4.0.0",
|
||||
"pump": "3.0.0"
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"templates": [
|
||||
{
|
||||
"filename": "custom-full-feature-image",
|
||||
"name": "Full Feature Image",
|
||||
"for": [
|
||||
"page",
|
||||
"post"
|
||||
],
|
||||
"slug": null
|
||||
},
|
||||
{
|
||||
"filename": "custom-narrow-feature-image",
|
||||
"name": "Narrow Feature Image",
|
||||
"for": [
|
||||
"page",
|
||||
"post"
|
||||
],
|
||||
"slug": null
|
||||
},
|
||||
{
|
||||
"filename": "custom-no-feature-image",
|
||||
"name": "No Feature Image",
|
||||
"for": [
|
||||
"page",
|
||||
"post"
|
||||
],
|
||||
"slug": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
52
apps/admin-x-settings/test/utils/responses/tiers.json
Normal file
52
apps/admin-x-settings/test/utils/responses/tiers.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"tiers": [
|
||||
{
|
||||
"id": "645453f4d254799990dd0e22",
|
||||
"name": "Basic Supporter",
|
||||
"description": null,
|
||||
"slug": "default-product",
|
||||
"active": true,
|
||||
"type": "paid",
|
||||
"welcome_page_url": null,
|
||||
"created_at": "2023-05-05T00:55:16.000Z",
|
||||
"updated_at": "2023-05-05T01:14:35.000Z",
|
||||
"visibility": "public",
|
||||
"benefits": [
|
||||
"Simple benefit"
|
||||
],
|
||||
"currency": "USD",
|
||||
"monthly_price": 500,
|
||||
"yearly_price": 5000,
|
||||
"trial_days": 0
|
||||
},
|
||||
{
|
||||
"id": "649a4f08e1de1c862cd79063",
|
||||
"name": "Ultimate Starlight Diamond Supporter",
|
||||
"description": null,
|
||||
"slug": "ultimate-starlight-diamond-supporter",
|
||||
"active": true,
|
||||
"type": "paid",
|
||||
"welcome_page_url": null,
|
||||
"created_at": "2023-06-27T02:52:56.067Z",
|
||||
"updated_at": null,
|
||||
"visibility": "none",
|
||||
"benefits": [
|
||||
"Awesome benefit"
|
||||
],
|
||||
"currency": "USD",
|
||||
"monthly_price": 1000,
|
||||
"yearly_price": 10000,
|
||||
"trial_days": 0
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"limit": 2,
|
||||
"total": 2,
|
||||
"prev": null,
|
||||
"next": null
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue