mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 06:35:49 +03:00
Added tests for tier edit modal (#17369)
refs https://github.com/TryGhost/Product/issues/3580
This commit is contained in:
parent
704fc18856
commit
0137f498d7
@ -43,7 +43,8 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
const [errors, setErrors] = useState<{ [key in keyof Tier]?: string }>({}); // eslint-disable-line no-unused-vars
|
||||
|
||||
const setError = (field: keyof Tier, error: string | undefined) => {
|
||||
setErrors({...errors, [field]: error});
|
||||
setErrors(errs => ({...errs, [field]: error}));
|
||||
return error;
|
||||
};
|
||||
|
||||
const {formState, updateForm, handleSave} = useForm<TierFormState>({
|
||||
@ -55,14 +56,6 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
currency: tier?.currency || currencies[0].isoCode
|
||||
},
|
||||
onSave: async () => {
|
||||
if (Object.values(errors).some(error => error)) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const {monthly_price: monthlyPrice, yearly_price: yearlyPrice, trial_days: trialDays, currency, ...rest} = formState;
|
||||
const values: Partial<Tier> = rest;
|
||||
|
||||
@ -85,6 +78,14 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
}
|
||||
});
|
||||
|
||||
const currencySymbol = formState.currency ? getSymbol(formState.currency) : '$';
|
||||
|
||||
const validators = {
|
||||
name: () => setError('name', formState.name ? undefined : 'You must specify a name'),
|
||||
monthly_price: () => setError('monthly_price', (isFreeTier || (formState.monthly_price && parseFloat(formState.monthly_price) >= 1)) ? undefined : `Subscription amount must be at least ${currencySymbol}1.00`),
|
||||
yearly_price: () => setError('yearly_price', (isFreeTier || (formState.yearly_price && parseFloat(formState.yearly_price) >= 1)) ? undefined : `Subscription amount must be at least ${currencySymbol}1.00`)
|
||||
};
|
||||
|
||||
const benefits = useSortableIndexedList({
|
||||
items: formState.benefits || [],
|
||||
setItems: newBenefits => updateForm(state => ({...state, benefits: newBenefits})),
|
||||
@ -96,17 +97,26 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
return value.match(/[\d]+\.?[\d]{0,2}/)?.[0] || '';
|
||||
};
|
||||
|
||||
const currencySymbol = formState.currency ? getSymbol(formState.currency) : '$';
|
||||
|
||||
return <Modal
|
||||
afterClose={() => {
|
||||
updateRoute('tiers');
|
||||
}}
|
||||
okLabel='Save & close'
|
||||
size='lg'
|
||||
testId='tier-detail-modal'
|
||||
title='Tier'
|
||||
stickyFooter
|
||||
onOk={handleSave}
|
||||
onOk={() => {
|
||||
if (Object.values(validators).filter(validator => validator()).length) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handleSave();
|
||||
}}
|
||||
>
|
||||
<div className='mt-8 flex items-start gap-16'>
|
||||
<div className='flex grow flex-col gap-5'>
|
||||
@ -118,7 +128,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
placeholder='Bronze'
|
||||
title='Name'
|
||||
value={formState.name || ''}
|
||||
onBlur={e => setError('name', e.target.value ? undefined : 'You must specify a name')}
|
||||
onBlur={() => validators.name()}
|
||||
onChange={e => updateForm(state => ({...state, name: e.target.value}))}
|
||||
/>}
|
||||
<TextField
|
||||
@ -155,8 +165,10 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
hint={errors.monthly_price}
|
||||
placeholder='1'
|
||||
rightPlaceholder={`${formState.currency}/month`}
|
||||
title='Monthly price'
|
||||
value={formState.monthly_price}
|
||||
onBlur={e => setError('monthly_price', e.target.value ? undefined : `Subscription amount must be at least ${currencySymbol}1.00`)}
|
||||
hideTitle
|
||||
onBlur={() => validators.monthly_price()}
|
||||
onChange={e => updateForm(state => ({...state, monthly_price: forceCurrencyValue(e.target.value)}))}
|
||||
/>
|
||||
<TextField
|
||||
@ -164,16 +176,17 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
hint={errors.yearly_price}
|
||||
placeholder='10'
|
||||
rightPlaceholder={`${formState.currency}/year`}
|
||||
title='Yearly price'
|
||||
value={formState.yearly_price}
|
||||
onBlur={e => setError('yearly_price', e.target.value ? undefined : `Subscription amount must be at least ${currencySymbol}1.00`)}
|
||||
hideTitle
|
||||
onBlur={() => validators.yearly_price()}
|
||||
onChange={e => updateForm(state => ({...state, yearly_price: forceCurrencyValue(e.target.value)}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='basis-1/2'>
|
||||
<div className='mb-1 flex h-6 items-center justify-between'>
|
||||
<Heading level={6}>Add a free trial</Heading>
|
||||
<Toggle onChange={e => setHasFreeTrial(e.target.checked)} />
|
||||
<div className='mb-1 flex h-6 flex-col justify-center'>
|
||||
<Toggle label='Add a free trial' labelStyle='heading' onChange={e => setHasFreeTrial(e.target.checked)} />
|
||||
</div>
|
||||
<TextField
|
||||
disabled={!hasFreeTrial}
|
||||
@ -182,7 +195,9 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
</>}
|
||||
placeholder='0'
|
||||
rightPlaceholder='days'
|
||||
title='Trial days'
|
||||
value={formState.trial_days}
|
||||
hideTitle
|
||||
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/[^\d]/, '')}))}
|
||||
/>
|
||||
</div>
|
||||
@ -210,10 +225,21 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||
<TextField
|
||||
className='grow'
|
||||
placeholder='Expert analysis'
|
||||
title='New benefit'
|
||||
value={benefits.newItem}
|
||||
hideTitle
|
||||
onChange={e => benefits.setNewItem(e.target.value)}
|
||||
/>
|
||||
<Button className='absolute right-0 top-1' color='green' icon="add" iconColorClass='text-white' size='sm' onClick={() => benefits.addItem()} />
|
||||
<Button
|
||||
className='absolute right-0 top-1'
|
||||
color='green'
|
||||
icon='add'
|
||||
iconColorClass='text-white'
|
||||
label='Add'
|
||||
size='sm'
|
||||
hideLabel
|
||||
onClick={() => benefits.addItem()}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -29,7 +29,7 @@ const TierCard: React.FC<TierCardProps> = ({
|
||||
const currencySymbol = currency ? getSymbol(currency) : '$';
|
||||
|
||||
return (
|
||||
<div className={cardContainerClasses}>
|
||||
<div className={cardContainerClasses} data-testid='tier-card'>
|
||||
<div className='w-full grow cursor-pointer' onClick={() => {
|
||||
NiceModal.show(TierDetailModal, {tier});
|
||||
}}>
|
||||
@ -81,7 +81,7 @@ const TiersList: React.FC<TiersListProps> = ({
|
||||
return <TierCard tier={tier} updateTier={updateTier} />;
|
||||
})}
|
||||
{tab === 'active-tiers' && (
|
||||
<div className={`${cardContainerClasses} group cursor-pointer`} onClick={() => {
|
||||
<button className={`${cardContainerClasses} group cursor-pointer`} type='button' onClick={() => {
|
||||
openTierModal();
|
||||
}}>
|
||||
<div className='flex h-full w-full flex-col items-center justify-center'>
|
||||
@ -90,7 +90,7 @@ const TiersList: React.FC<TiersListProps> = ({
|
||||
<div className='mt-2 translate-y-[-10px] text-sm font-semibold text-green opacity-0 transition-all group-hover:translate-y-0 group-hover:opacity-100'>Add tier</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -64,7 +64,7 @@ test.describe('Access settings', async () => {
|
||||
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.locator('[data-testid="multiselect-option"]', {hasText: 'Ultimate Starlight Diamond Tier'}).click();
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
@ -73,7 +73,7 @@ test.describe('Access settings', async () => {
|
||||
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))}
|
||||
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.slice(1).map(tier => tier.id))}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
168
apps/admin-x-settings/test/e2e/membership/tiers.test.ts
Normal file
168
apps/admin-x-settings/test/e2e/membership/tiers.test.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '../../utils/e2e';
|
||||
|
||||
test.describe('Tier settings', async () => {
|
||||
test('Supports creating a new tier', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
tiers: {
|
||||
add: {
|
||||
tiers: [{
|
||||
id: 'new-tier',
|
||||
type: 'paid',
|
||||
active: true,
|
||||
name: 'Plus tier',
|
||||
slug: 'plus-tier',
|
||||
description: null,
|
||||
monthly_price: 800,
|
||||
yearly_price: 8000,
|
||||
benefits: [],
|
||||
welcome_page_url: null,
|
||||
trial_days: 0,
|
||||
visibility: 'public',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('tiers');
|
||||
|
||||
await section.getByRole('button', {name: 'Add tier'}).click();
|
||||
|
||||
const modal = page.getByTestId('tier-detail-modal');
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/One or more fields have errors/);
|
||||
await expect(modal).toHaveText(/You must specify a name/);
|
||||
await expect(modal).toHaveText(/Subscription amount must be at least \$1\.00/);
|
||||
|
||||
await modal.getByLabel('Name').fill('Plus tier');
|
||||
await modal.getByLabel('Monthly price').fill('8');
|
||||
await modal.getByLabel('Yearly price').fill('80');
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/Plus tier/);
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/\$8\/month/);
|
||||
|
||||
expect(lastApiRequests.tiers.add.body).toMatchObject({
|
||||
tiers: [{
|
||||
name: 'Plus tier',
|
||||
monthly_price: 800,
|
||||
yearly_price: 8000,
|
||||
trial_days: null
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports updating a tier', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
tiers: {
|
||||
edit: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[1],
|
||||
name: 'Supporter updated',
|
||||
description: 'Supporter description',
|
||||
monthly_price: 1001,
|
||||
trial_days: 7,
|
||||
benefits: [
|
||||
'Simple benefit',
|
||||
'New benefit'
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('tiers');
|
||||
|
||||
await section.getByTestId('tier-card').filter({hasText: /Supporter/}).click();
|
||||
|
||||
const modal = page.getByTestId('tier-detail-modal');
|
||||
|
||||
await modal.getByLabel('Name').fill('');
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast')).toHaveText(/One or more fields have errors/);
|
||||
await expect(modal).toHaveText(/You must specify a name/);
|
||||
|
||||
await modal.getByLabel('Name').fill('Supporter updated');
|
||||
await modal.getByLabel('Description').fill('Supporter description');
|
||||
await modal.getByLabel('Monthly price').fill('10.01');
|
||||
await modal.getByLabel('Add a free trial').check();
|
||||
await modal.getByLabel('Trial days').fill('7');
|
||||
await modal.getByLabel('New benefit').fill('New benefit');
|
||||
await modal.getByRole('button', {name: 'Add'}).click();
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Supporter/})).toHaveText(/Supporter updated/);
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Supporter/})).toHaveText(/Supporter description/);
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Supporter/})).toHaveText(/\$10\.01\/month/);
|
||||
|
||||
expect(lastApiRequests.tiers.edit.body).toMatchObject({
|
||||
tiers: [{
|
||||
id: responseFixtures.tiers.tiers[1].id,
|
||||
name: 'Supporter updated',
|
||||
description: 'Supporter description',
|
||||
monthly_price: 1001,
|
||||
trial_days: 7,
|
||||
benefits: [
|
||||
'Simple benefit',
|
||||
'New benefit'
|
||||
]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
test('Supports editing the free tier', async ({page}) => {
|
||||
const lastApiRequests = await mockApi({page, responses: {
|
||||
tiers: {
|
||||
edit: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[0],
|
||||
description: 'Free tier description',
|
||||
benefits: [
|
||||
'First benefit',
|
||||
'Second benefit'
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const section = page.getByTestId('tiers');
|
||||
|
||||
await section.getByTestId('tier-card').filter({hasText: /Free/}).click();
|
||||
|
||||
const modal = page.getByTestId('tier-detail-modal');
|
||||
|
||||
await modal.getByLabel('Description').fill('Free tier description');
|
||||
await modal.getByLabel('New benefit').fill('First benefit');
|
||||
await modal.getByRole('button', {name: 'Add'}).click();
|
||||
await modal.getByLabel('New benefit').fill('Second benefit');
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(section.getByTestId('tier-card').filter({hasText: /Free/})).toHaveText(/Free tier description/);
|
||||
|
||||
expect(lastApiRequests.tiers.edit.body).toMatchObject({
|
||||
tiers: [{
|
||||
id: responseFixtures.tiers.tiers[0].id,
|
||||
description: 'Free tier description',
|
||||
benefits: [
|
||||
'First benefit',
|
||||
'Second benefit'
|
||||
]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
const assert = require('assert/strict');
|
||||
|
||||
describe('Hello world', function () {
|
||||
it('Runs a test', function () {
|
||||
// TODO: Write me!
|
||||
assert.ok(require('../index'));
|
||||
});
|
||||
});
|
@ -56,6 +56,8 @@ interface Responses {
|
||||
}
|
||||
tiers?: {
|
||||
browse?: TiersResponseType
|
||||
edit?: TiersResponseType
|
||||
add?: TiersResponseType
|
||||
}
|
||||
labels?: {
|
||||
browse?: LabelsResponseType
|
||||
@ -121,6 +123,8 @@ type LastRequests = {
|
||||
}
|
||||
tiers: {
|
||||
browse: RequestRecord
|
||||
edit: RequestRecord
|
||||
add: RequestRecord
|
||||
}
|
||||
labels: {
|
||||
browse: RequestRecord
|
||||
@ -152,7 +156,7 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
||||
images: {upload: {}},
|
||||
customThemeSettings: {browse: {}, edit: {}},
|
||||
latestPost: {browse: {}},
|
||||
tiers: {browse: {}},
|
||||
tiers: {browse: {}, edit: {}, add: {}},
|
||||
labels: {browse: {}},
|
||||
offers: {browse: {}},
|
||||
themes: {browse: {}, activate: {}, delete: {}, install: {}, upload: {}},
|
||||
@ -386,7 +390,29 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/tiers\//,
|
||||
path: /\/ghost\/api\/admin\/tiers\/\w{24}/,
|
||||
respondTo: {
|
||||
PUT: {
|
||||
body: responses?.tiers?.edit ?? responseFixtures.tiers,
|
||||
updateLastRequest: lastApiRequests.tiers.edit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/tiers\/$/,
|
||||
respondTo: {
|
||||
POST: {
|
||||
body: responses?.tiers?.add ?? responseFixtures.tiers,
|
||||
updateLastRequest: lastApiRequests.tiers.add
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/tiers\/\?limit/,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.tiers?.browse ?? responseFixtures.tiers,
|
||||
|
@ -1,5 +1,19 @@
|
||||
{
|
||||
"tiers": [
|
||||
{
|
||||
"id": "645453f4d254799990dd0e21",
|
||||
"name": "Free",
|
||||
"description": null,
|
||||
"slug": "free",
|
||||
"active": true,
|
||||
"type": "free",
|
||||
"welcome_page_url": null,
|
||||
"created_at": "2023-05-05T00:55:16.000Z",
|
||||
"updated_at": "2023-05-08T06:08:47.000Z",
|
||||
"visibility": "public",
|
||||
"benefits": [],
|
||||
"trial_days": 0
|
||||
},
|
||||
{
|
||||
"id": "645453f4d254799990dd0e22",
|
||||
"name": "Basic Supporter",
|
||||
@ -21,9 +35,9 @@
|
||||
},
|
||||
{
|
||||
"id": "649a4f08e1de1c862cd79063",
|
||||
"name": "Ultimate Starlight Diamond Supporter",
|
||||
"name": "Ultimate Starlight Diamond Tier",
|
||||
"description": null,
|
||||
"slug": "ultimate-starlight-diamond-supporter",
|
||||
"slug": "ultimate-starlight-diamond-tier",
|
||||
"active": true,
|
||||
"type": "paid",
|
||||
"welcome_page_url": null,
|
||||
|
Loading…
Reference in New Issue
Block a user