tests(upload): fix error logs in upload tests (#18261)

* chore: add test utils

* test(upload): fix useModalQueryParams

* test(upload): fix TableRows

* test(upload): fix useConfig test

* test(upload): fix useFolder tests

* test(upload): fix useFolders test

* test(upload): fix SettingsPage tests

* test(upload): fix UploadAssetDialog tests

* test(upload): fix CarouselAssets tests
This commit is contained in:
Josh 2023-10-04 16:03:02 +01:00 committed by GitHub
parent eeb77afbca
commit ddcbafe9d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 790 additions and 3237 deletions

View File

@ -1,19 +1,9 @@
import React from 'react'; import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system'; import { render, waitFor } from '@tests/utils';
import { fireEvent, render } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { CarouselAssets } from '../CarouselAssets'; import { CarouselAssets } from '../CarouselAssets';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn(() => ({
toggleNotification: jest.fn(),
})),
}));
const ASSET_FIXTURES = [ const ASSET_FIXTURES = [
{ {
alternativeText: 'alternative text', alternativeText: 'alternative text',
@ -33,19 +23,8 @@ const ASSET_FIXTURES = [
}, },
]; ];
const client = new QueryClient({ const setup = (props) =>
defaultOptions: { render(
queries: {
retry: false,
},
},
});
const ComponentFixture = (props) => {
return (
<QueryClientProvider client={client}>
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<CarouselAssets <CarouselAssets
assets={ASSET_FIXTURES} assets={ASSET_FIXTURES}
label="Carousel" label="Carousel"
@ -58,13 +37,7 @@ const ComponentFixture = (props) => {
selectedAssetIndex={0} selectedAssetIndex={0}
{...props} {...props}
/> />
</ThemeProvider>
</IntlProvider>
</QueryClientProvider>
); );
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('MediaLibraryInput | Carousel | CarouselAssets', () => { describe('MediaLibraryInput | Carousel | CarouselAssets', () => {
it('should render empty carousel', () => { it('should render empty carousel', () => {
@ -90,30 +63,32 @@ describe('MediaLibraryInput | Carousel | CarouselAssets', () => {
expect(getByRole('button', { name: 'edit' })).toBeInTheDocument(); expect(getByRole('button', { name: 'edit' })).toBeInTheDocument();
}); });
it('should call onAddAsset', () => { it('should call onAddAsset', async () => {
const onAddAssetSpy = jest.fn(); const onAddAssetSpy = jest.fn();
const { getByRole } = setup({ onAddAsset: onAddAssetSpy }); const { getByRole, user } = setup({ onAddAsset: onAddAssetSpy });
fireEvent.click(getByRole('button', { name: 'Add' })); await user.click(getByRole('button', { name: 'Add' }));
expect(onAddAssetSpy).toHaveBeenCalledTimes(1); expect(onAddAssetSpy).toHaveBeenCalledTimes(1);
}); });
it('should call onDeleteAsset', () => { it('should call onDeleteAsset', async () => {
const onDeleteAssetSpy = jest.fn(); const onDeleteAssetSpy = jest.fn();
const { getByRole } = setup({ onDeleteAsset: onDeleteAssetSpy }); const { getByRole, user } = setup({ onDeleteAsset: onDeleteAssetSpy });
fireEvent.click(getByRole('button', { name: 'Delete' })); await user.click(getByRole('button', { name: 'Delete' }));
expect(onDeleteAssetSpy).toHaveBeenCalledTimes(1); expect(onDeleteAssetSpy).toHaveBeenCalledTimes(1);
}); });
it('should open edit view', () => { it('should open edit view', async () => {
const { getByRole, getByText } = setup(); const { getByRole, getByText, user, queryByText } = setup();
fireEvent.click(getByRole('button', { name: 'edit' })); await user.click(getByRole('button', { name: 'edit' }));
expect(getByText('Details')).toBeInTheDocument(); expect(getByText('Details')).toBeInTheDocument();
await waitFor(() => expect(queryByText('Content is loading.')).not.toBeInTheDocument());
}); });
it('should render the localized label', () => { it('should render the localized label', () => {

View File

@ -46,7 +46,7 @@ export const TableRows = ({
fn: () => handleRowClickFn(element, contentType, id, path), fn: () => handleRowClickFn(element, contentType, id, path),
})} })}
> >
<Td {...stopPropagation}> <Td onClick={(e) => e.stopPropagation()}>
<BaseCheckbox <BaseCheckbox
aria-label={formatMessage( aria-label={formatMessage(
{ {

View File

@ -1,17 +1,10 @@
import React from 'react'; import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system'; import { RawTable } from '@strapi/design-system';
import { fireEvent, render } from '@testing-library/react'; import { render, fireEvent } from '@tests/utils';
import { IntlProvider } from 'react-intl';
import { MemoryRouter } from 'react-router-dom';
import { TableRows } from '../TableRows'; import { TableRows } from '../TableRows';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useQueryParams: jest.fn(() => [{ query: {} }]),
}));
const ASSET_FIXTURE = { const ASSET_FIXTURE = {
alternativeText: 'alternative text', alternativeText: 'alternative text',
createdAt: '2021-10-01T08:04:56.326Z', createdAt: '2021-10-01T08:04:56.326Z',
@ -32,7 +25,7 @@ const ASSET_FIXTURE = {
const FOLDER_FIXTURE = { const FOLDER_FIXTURE = {
createdAt: '2022-11-17T10:40:06.022Z', createdAt: '2022-11-17T10:40:06.022Z',
id: 1, id: 2,
name: 'folder 1', name: 'folder 1',
type: 'folder', type: 'folder',
updatedAt: '2022-11-17T10:40:06.022Z', updatedAt: '2022-11-17T10:40:06.022Z',
@ -47,26 +40,12 @@ const PROPS_FIXTURE = {
selected: [], selected: [],
}; };
const ComponentFixture = (props) => { const setup = (props) =>
const customProps = { render(<TableRows {...PROPS_FIXTURE} {...props} />, {
...PROPS_FIXTURE, renderOptions: {
...props, wrapper: ({ children }) => <RawTable>{children}</RawTable>,
}; },
});
return (
<MemoryRouter>
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<table>
<TableRows {...customProps} />
</table>
</ThemeProvider>
</IntlProvider>
</MemoryRouter>
);
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('TableList | TableRows', () => { describe('TableList | TableRows', () => {
describe('rendering assets', () => { describe('rendering assets', () => {
@ -85,7 +64,10 @@ describe('TableList | TableRows', () => {
const onSelectOneSpy = jest.fn(); const onSelectOneSpy = jest.fn();
const { getByRole } = setup({ onSelectOne: onSelectOneSpy }); const { getByRole } = setup({ onSelectOne: onSelectOneSpy });
fireEvent.click(getByRole('checkbox', { name: 'Select michka asset', hidden: true })); /**
* using UserEvent never triggers the onChange event.
*/
fireEvent.click(getByRole('checkbox', { name: 'Select michka asset' }));
expect(onSelectOneSpy).toHaveBeenCalledTimes(1); expect(onSelectOneSpy).toHaveBeenCalledTimes(1);
}); });
@ -93,34 +75,32 @@ describe('TableList | TableRows', () => {
it('should reflect non selected assets state', () => { it('should reflect non selected assets state', () => {
const { getByRole } = setup(); const { getByRole } = setup();
expect( expect(getByRole('checkbox', { name: 'Select michka asset' })).not.toBeChecked();
getByRole('checkbox', { name: 'Select michka asset', hidden: true })
).not.toBeChecked();
}); });
it('should reflect selected assets state', () => { it('should reflect selected assets state', () => {
const { getByRole } = setup({ selected: [{ id: 1, type: 'asset' }] }); const { getByRole } = setup({ selected: [{ id: 1, type: 'asset' }] });
expect(getByRole('checkbox', { name: 'Select michka asset', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeChecked();
}); });
it('should disable select asset checkbox when users do not have the permission to update', () => { it('should disable select asset checkbox when users do not have the permission to update', () => {
const { getByRole } = setup({ canUpdate: false }); const { getByRole } = setup({ canUpdate: false });
expect(getByRole('checkbox', { name: 'Select michka asset', hidden: true })).toBeDisabled(); expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeDisabled();
}); });
it('should disable select asset checkbox when users if the file type is not allowed', () => { it('should disable select asset checkbox when users if the file type is not allowed', () => {
const { getByRole } = setup({ allowedTypes: [] }); const { getByRole } = setup({ allowedTypes: [] });
expect(getByRole('checkbox', { name: 'Select michka asset', hidden: true })).toBeDisabled(); expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeDisabled();
}); });
it('should call onEditAsset callback', () => { it('should call onEditAsset callback', async () => {
const onEditAssetSpy = jest.fn(); const onEditAssetSpy = jest.fn();
const { getByRole } = setup({ onEditAsset: onEditAssetSpy }); const { getByRole, user } = setup({ onEditAsset: onEditAssetSpy });
fireEvent.click(getByRole('button', { name: 'Edit', hidden: true })); await user.click(getByRole('button', { name: 'Edit', hidden: true }));
expect(onEditAssetSpy).toHaveBeenCalledTimes(1); expect(onEditAssetSpy).toHaveBeenCalledTimes(1);
}); });
@ -135,14 +115,14 @@ describe('TableList | TableRows', () => {
expect(getByText('folder 1')).toBeInTheDocument(); expect(getByText('folder 1')).toBeInTheDocument();
}); });
it('should call onEditFolder callback', () => { it('should call onEditFolder callback', async () => {
const onEditFolderSpy = jest.fn(); const onEditFolderSpy = jest.fn();
const { getByRole } = setup({ const { getByRole, user } = setup({
rows: [FOLDER_FIXTURE], rows: [FOLDER_FIXTURE],
onEditFolder: onEditFolderSpy, onEditFolder: onEditFolderSpy,
}); });
fireEvent.click(getByRole('button', { name: 'Edit', hidden: true })); await user.click(getByRole('button', { name: 'Edit', hidden: true }));
expect(onEditFolderSpy).toHaveBeenCalledTimes(1); expect(onEditFolderSpy).toHaveBeenCalledTimes(1);
}); });
@ -159,13 +139,16 @@ describe('TableList | TableRows', () => {
expect(getByRole('button', { name: 'Access folder', hidden: true })).toBeInTheDocument(); expect(getByRole('button', { name: 'Access folder', hidden: true })).toBeInTheDocument();
}); });
it('should call onChangeFolder when clicking on folder navigation button', () => { it('should call onChangeFolder when clicking on folder navigation button', async () => {
const onChangeFolderSpy = jest.fn(); const onChangeFolderSpy = jest.fn();
const { getByRole } = setup({ rows: [FOLDER_FIXTURE], onChangeFolder: onChangeFolderSpy }); const { getByRole, user } = setup({
rows: [FOLDER_FIXTURE],
onChangeFolder: onChangeFolderSpy,
});
fireEvent.click(getByRole('button', { name: 'Access folder', hidden: true })); await user.click(getByRole('button', { name: 'Access folder', hidden: true }));
expect(onChangeFolderSpy).toHaveBeenCalledWith(1); expect(onChangeFolderSpy).toHaveBeenCalledWith(2);
}); });
it('should reflect non selected folder state', () => { it('should reflect non selected folder state', () => {
@ -179,7 +162,7 @@ describe('TableList | TableRows', () => {
it('should reflect selected folder state', () => { it('should reflect selected folder state', () => {
const { getByRole } = setup({ const { getByRole } = setup({
rows: [FOLDER_FIXTURE], rows: [FOLDER_FIXTURE],
selected: [{ id: 1, type: 'folder' }], selected: [{ id: 2, type: 'folder' }],
}); });
expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked();
@ -208,17 +191,15 @@ describe('TableList | TableRows', () => {
}); });
}); });
describe.only('rendering folder & asset with the same id', () => { describe('rendering folder & asset with the same id', () => {
it('should reflect selected only folder state', () => { it('should reflect selected only folder state', () => {
const { getByRole } = setup({ const { getByRole } = setup({
rows: [FOLDER_FIXTURE, ASSET_FIXTURE], rows: [FOLDER_FIXTURE, ASSET_FIXTURE],
selected: [{ id: 1, type: 'folder' }], selected: [{ id: 2, type: 'folder' }],
}); });
expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked();
expect( expect(getByRole('checkbox', { name: 'Select michka asset' })).not.toBeChecked();
getByRole('checkbox', { name: 'Select michka asset', hidden: true })
).not.toBeChecked();
}); });
it('should reflect selected only asset state', () => { it('should reflect selected only asset state', () => {
@ -230,7 +211,7 @@ describe('TableList | TableRows', () => {
expect( expect(
getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true }) getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })
).not.toBeChecked(); ).not.toBeChecked();
expect(getByRole('checkbox', { name: 'Select michka asset', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeChecked();
}); });
it('should reflect selected both asset & folder state', () => { it('should reflect selected both asset & folder state', () => {
@ -238,12 +219,12 @@ describe('TableList | TableRows', () => {
rows: [FOLDER_FIXTURE, ASSET_FIXTURE], rows: [FOLDER_FIXTURE, ASSET_FIXTURE],
selected: [ selected: [
{ id: 1, type: 'asset' }, { id: 1, type: 'asset' },
{ id: 1, type: 'folder' }, { id: 2, type: 'folder' },
], ],
}); });
expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select folder 1 folder', hidden: true })).toBeChecked();
expect(getByRole('checkbox', { name: 'Select michka asset', hidden: true })).toBeChecked(); expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeChecked();
}); });
}); });
}); });

View File

@ -134,6 +134,7 @@ UploadAssetDialog.defaultProps = {
addUploadedFiles: undefined, addUploadedFiles: undefined,
folderId: null, folderId: null,
initialAssetsToAdd: undefined, initialAssetsToAdd: undefined,
onClose() {},
trackedLocation: undefined, trackedLocation: undefined,
validateAssetsTypes: undefined, validateAssetsTypes: undefined,
}; };
@ -142,7 +143,7 @@ UploadAssetDialog.propTypes = {
addUploadedFiles: PropTypes.func, addUploadedFiles: PropTypes.func,
folderId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), folderId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
initialAssetsToAdd: PropTypes.arrayOf(AssetDefinition), initialAssetsToAdd: PropTypes.arrayOf(AssetDefinition),
onClose: PropTypes.func.isRequired, onClose: PropTypes.func,
trackedLocation: PropTypes.string, trackedLocation: PropTypes.string,
validateAssetsTypes: PropTypes.func, validateAssetsTypes: PropTypes.func,
}; };

View File

@ -1,81 +1,41 @@
/* eslint-disable no-await-in-loop */
import React from 'react'; import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system'; import { within } from '@testing-library/react';
import { TrackingProvider } from '@strapi/helper-plugin'; import { fireEvent, render, screen, waitFor } from '@tests/utils';
import { fireEvent, render as renderTL, screen, waitFor, within } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import en from '../../../translations/en.json';
import { UploadAssetDialog } from '../UploadAssetDialog'; import { UploadAssetDialog } from '../UploadAssetDialog';
import { server } from './server';
jest.mock('../../../utils/getTrad', () => (x) => x);
jest.mock('react-intl', () => ({
useIntl: () => ({ formatMessage: jest.fn(({ id }) => en[id] || id) }),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
const render = (props = { onClose() {} }) =>
renderTL(
<QueryClientProvider client={queryClient}>
<TrackingProvider>
<ThemeProvider theme={lightTheme}>
<UploadAssetDialog {...props} />
</ThemeProvider>
</TrackingProvider>
</QueryClientProvider>,
{ container: document.getElementById('app') }
);
describe('UploadAssetDialog', () => { describe('UploadAssetDialog', () => {
let confirmSpy; let confirmSpy;
beforeAll(() => { beforeAll(() => {
confirmSpy = jest.spyOn(window, 'confirm'); confirmSpy = jest.spyOn(window, 'confirm');
confirmSpy.mockImplementation(jest.fn(() => true)); confirmSpy.mockImplementation(jest.fn(() => true));
server.listen();
}); });
afterEach(() => server.resetHandlers());
afterAll(() => { afterAll(() => {
confirmSpy.mockRestore(); confirmSpy.mockRestore();
server.close();
}); });
describe('from computer', () => { describe('from computer', () => {
it('snapshots the component', () => { it('closes the dialog when clicking on cancel on the add asset step', async () => {
render();
expect(document.body).toMatchSnapshot();
});
it('closes the dialog when clicking on cancel on the add asset step', () => {
const onCloseSpy = jest.fn(); const onCloseSpy = jest.fn();
render({ onClose: onCloseSpy, onSuccess() {} }); const { user, getByRole } = render(<UploadAssetDialog onClose={onCloseSpy} />);
fireEvent.click(screen.getByText('app.components.Button.cancel')); await user.click(getByRole('button', { name: 'cancel' }));
expect(onCloseSpy).toBeCalled(); expect(onCloseSpy).toBeCalled();
}); });
it('open confirm box when clicking on cancel on the pending asset step', () => { it('open confirm box when clicking on cancel on the pending asset step', async () => {
const file = new File(['Some stuff'], 'test.png', { type: 'image/png' }); const file = new File(['Some stuff'], 'test.png', { type: 'image/png' });
const onCloseSpy = jest.fn();
render({ onClose: onCloseSpy, onSuccess() {} }); const { user, getByRole } = render(<UploadAssetDialog />);
const fileList = [file]; await user.upload(document.querySelector('[type="file"]'), file);
fileList.item = (i) => fileList[i];
fireEvent.change(document.querySelector('[type="file"]'), { target: { files: fileList } }); await user.click(getByRole('button', { name: 'cancel' }));
fireEvent.click(screen.getByText('app.components.Button.cancel'));
expect(window.confirm).toBeCalled(); expect(window.confirm).toBeCalled();
}); });
@ -86,53 +46,30 @@ describe('UploadAssetDialog', () => {
['pdf', 'application/pdf', 'Doc', 1], ['pdf', 'application/pdf', 'Doc', 1],
['unknown', 'unknown', 'Doc', 1], ['unknown', 'unknown', 'Doc', 1],
].forEach(([ext, mime, assetType, number]) => { ].forEach(([ext, mime, assetType, number]) => {
it(`shows ${number} valid ${mime} file`, () => { it(`shows ${number} valid ${mime} file`, async () => {
const onCloseSpy = jest.fn();
// see https://github.com/testing-library/react-testing-library/issues/470
Object.defineProperty(HTMLMediaElement.prototype, 'muted', {
set() {},
});
const file = new File(['Some stuff'], `test.${ext}`, { type: mime }); const file = new File(['Some stuff'], `test.${ext}`, { type: mime });
const fileList = [file]; const { user, getByText, getAllByText } = render(<UploadAssetDialog />);
fileList.item = (i) => fileList[i];
render({ onClose: onCloseSpy, onSuccess() {} }); await user.upload(document.querySelector('[type="file"]'), file);
fireEvent.change(document.querySelector('[type="file"]'), { expect(getByText('1 asset ready to upload')).toBeInTheDocument();
target: { files: fileList },
});
expect(screen.getAllByText(`Add new assets`).length).toBe(2);
expect( expect(
screen.getByText( getByText('Manage the assets before adding them to the Media Library')
'{number, plural, =0 {No asset} one {1 asset} other {# assets}} ready to upload'
)
).toBeInTheDocument(); ).toBeInTheDocument();
expect(
screen.getByText('Manage the assets before adding them to the Media Library') expect(getAllByText(`test.${ext}`).length).toBe(number);
).toBeInTheDocument(); expect(getByText(ext)).toBeInTheDocument();
expect(screen.getAllByText(`test.${ext}`).length).toBe(number); expect(getByText(assetType)).toBeInTheDocument();
expect(screen.getByText(ext)).toBeInTheDocument();
expect(screen.getByText(assetType)).toBeInTheDocument();
}); });
}); });
}); });
describe('from url', () => { describe('from url', () => {
it('snapshots the component', () => {
render();
fireEvent.click(screen.getByText('From url'));
expect(document.body).toMatchSnapshot();
});
it('shows an error message when the asset does not exist', async () => { it('shows an error message when the asset does not exist', async () => {
render(); const { user, getByRole } = render(<UploadAssetDialog />);
fireEvent.click(screen.getByText('From url'));
await user.click(getByRole('tab', { name: 'From URL' }));
const urls = [ const urls = [
'http://localhost:5000/an-image.png', 'http://localhost:5000/an-image.png',
@ -140,27 +77,48 @@ describe('UploadAssetDialog', () => {
'http://localhost:5000/a-video.mp4', 'http://localhost:5000/a-video.mp4',
'http://localhost:5000/not-working-like-cors.lutin', 'http://localhost:5000/not-working-like-cors.lutin',
'http://localhost:1234/some-where-not-existing.jpg', 'http://localhost:1234/some-where-not-existing.jpg',
].join('\n'); ];
fireEvent.change(screen.getByLabelText('URL'), { target: { value: urls } }); // eslint-disable-next-line no-restricted-syntax
fireEvent.click(screen.getByText('Next')); for (const url of urls) {
await user.type(getByRole('textbox', 'URL'), url);
await waitFor(() => expect(screen.getByText('Failed to fetch')).toBeInTheDocument()); await user.type(getByRole('textbox', 'URL'), '[Enter]');
}
/**
* userEvent does not submit forms.
*/
fireEvent.click(getByRole('button', { name: 'Next' }));
await waitFor(() => expect(screen.getByText('An error occured')).toBeInTheDocument());
}); });
it('snapshots the component with 4 URLs: 3 valid and one in failure', async () => { it('snapshots the component with 4 URLs: 3 valid and one in failure', async () => {
render(); const { user, getByText, getByRole } = render(<UploadAssetDialog />);
fireEvent.click(screen.getByText('From url'));
await user.click(getByRole('tab', { name: 'From URL' }));
const urls = [ const urls = [
'http://localhost:5000/an-image.png', 'http://localhost:5000/an-image.png',
'http://localhost:5000/a-pdf.pdf', 'http://localhost:5000/a-pdf.pdf',
'http://localhost:5000/a-video.mp4', 'http://localhost:5000/a-video.mp4',
'http://localhost:5000/not-working-like-cors.lutin', 'http://localhost:5000/not-working-like-cors.lutin',
].join('\n'); ];
fireEvent.change(screen.getByLabelText('URL'), { target: { value: urls } }); // eslint-disable-next-line no-restricted-syntax
fireEvent.click(screen.getByText('Next')); for (const url of urls) {
await user.type(getByRole('textbox', 'URL'), url);
if (urls.indexOf(url) < urls.length - 1) {
await user.type(getByRole('textbox', 'URL'), '[Enter]');
}
}
/**
* userEvent does not submit forms.
*/
fireEvent.click(getByRole('button', { name: 'Next' }));
const assets = [ const assets = [
{ {
@ -201,25 +159,14 @@ describe('UploadAssetDialog', () => {
}, },
]; ];
await waitFor(() => await waitFor(() => expect(getByText('4 assets ready to upload')).toBeInTheDocument());
expect(
screen.getByText(
'{number, plural, =0 {No asset} one {1 asset} other {# assets}} ready to upload'
)
).toBeInTheDocument()
);
expect(screen.getAllByText(`Add new assets`).length).toBe(2);
expect(
screen.getByText('Manage the assets before adding them to the Media Library')
).toBeInTheDocument();
assets.forEach((asset) => { assets.forEach((asset) => {
const dialog = within(screen.getByRole('dialog')); const card = within(screen.getByRole('dialog')).getAllByLabelText(asset.name)[0];
const card = within(dialog.getAllByLabelText(asset.name)[0]);
expect(card.getByText(asset.ext)).toBeInTheDocument(); expect(within(card).getByText(asset.ext)).toBeInTheDocument();
expect( expect(
card.getByText(asset.type.charAt(0).toUpperCase() + asset.type.slice(1)) within(card).getByText(`${asset.type.charAt(0).toUpperCase()}${asset.type.slice(1)}`)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });

View File

@ -1,18 +0,0 @@
// mocking window.fetch since msw is not able to give back the res.url param
import { rest } from 'msw';
import { setupServer } from 'msw/node';
export const server = setupServer(
rest.get('*/an-image.png', (req, res, ctx) =>
res(ctx.set('Content-Type', 'image/png'), ctx.body())
),
rest.get('*/a-pdf.pdf', (req, res, ctx) =>
res(ctx.set('Content-Type', 'application/pdf'), ctx.body())
),
rest.get('*/a-video.mp4', (req, res, ctx) =>
res(ctx.set('Content-Type', 'video/mp4'), ctx.body())
),
rest.get('*/not-working-like-cors.lutin', (req, res, ctx) => res(ctx.json({}))),
rest.get('*/some-where-not-existing.jpg', (req, res) => res.networkError('Failed to fetch'))
);

View File

@ -1,183 +1,73 @@
import React from 'react'; import { act, renderHook, waitFor, screen, server } from '@tests/utils';
import { rest } from 'msw';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { NotificationsProvider, useFetchClient, useNotification } from '@strapi/helper-plugin';
import { act, renderHook, waitFor } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider, useQueryClient } from 'react-query';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { pageSizes, sortOptions } from '../../constants';
import pluginId from '../../pluginId';
import { useConfig } from '../useConfig'; import { useConfig } from '../useConfig';
const mockGetResponse = {
data: {
data: {
pageSize: pageSizes[0],
sort: sortOptions[0].value,
},
},
};
const notificationStatusMock = jest.fn();
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: () => notificationStatusMock,
useFetchClient: jest.fn().mockReturnValue({
put: jest.fn().mockResolvedValue({ data: { data: {} } }),
get: jest.fn(),
}),
}));
const refetchQueriesMock = jest.fn();
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: () => ({
refetchQueries: refetchQueriesMock,
}),
}));
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// eslint-disable-next-line react/prop-types
function ComponentFixture({ children }) {
return (
<Router>
<Route>
<QueryClientProvider client={client}>
<ThemeProvider theme={lightTheme}>
<NotificationsProvider>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</NotificationsProvider>
</ThemeProvider>
</QueryClientProvider>
</Route>
</Router>
);
}
function setup(...args) {
return new Promise((resolve) => {
act(() => {
resolve(renderHook(() => useConfig(...args), { wrapper: ComponentFixture }));
});
});
}
describe('useConfig', () => { describe('useConfig', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('query', () => { describe('query', () => {
test('does call the get endpoint', async () => { test('does call the get endpoint', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useConfig());
get.mockReturnValueOnce(mockGetResponse);
const { result } = await setup();
expect(get).toHaveBeenCalledWith(`/${pluginId}/configuration`);
await waitFor(() => expect(result.current.config.isLoading).toBe(false)); await waitFor(() => expect(result.current.config.isLoading).toBe(false));
expect(result.current.config.data).toEqual(mockGetResponse.data.data);
expect(result.current.config.data).toMatchInlineSnapshot(`
{
"pageSize": 20,
"sort": "updatedAt:DESC",
}
`);
}); });
test('should still return an object even if the server returns a falsey value', async () => { test('should still return an object even if the server returns a falsey value', async () => {
const { get } = useFetchClient(); server.use(
get.mockReturnValueOnce({ rest.get('/upload/configuration', (req, res, ctx) => {
data: { return res(
ctx.json({
data: null, data: null,
}, })
}); );
const { result } = await setup();
await waitFor(() => expect(result.current.config.data).toEqual({}));
});
test('calls toggleNotification in case of error', async () => {
const { get } = useFetchClient();
const originalConsoleError = console.error;
console.error = jest.fn();
get.mockRejectedValueOnce(new Error('Jest mock error'));
const toggleNotification = useNotification();
await setup({});
await waitFor(() =>
expect(toggleNotification).toBeCalledWith({
type: 'warning',
message: { id: 'notification.error' },
}) })
); );
const { result } = renderHook(() => useConfig());
await waitFor(() => expect(result.current.config.data).toEqual({}));
server.restoreHandlers();
});
test('calls toggleNotification in case of error', async () => {
const originalConsoleError = console.error;
console.error = jest.fn();
server.use(
rest.get('/upload/configuration', (req, res, ctx) => {
return res(ctx.status(500));
})
);
renderHook(() => useConfig());
await waitFor(() => expect(screen.getByText('notification.error')).toBeInTheDocument());
console.error = originalConsoleError; console.error = originalConsoleError;
server.restoreHandlers();
}); });
}); });
describe('mutation', () => { describe('mutation', () => {
test('does call the proper mutation endpoint', async () => { test('does call the proper mutation endpoint', async () => {
const { put } = useFetchClient(); const { result } = renderHook(() => useConfig());
const queryClient = useQueryClient();
let setupResult; act(() => {
await act(async () => { result.current.mutateConfig.mutateAsync({
setupResult = await setup(); pageSize: 100,
}); sort: 'name:DESC',
const {
result: {
current: { mutateConfig },
},
} = setupResult;
const mutateWith = {};
await act(async () => {
await mutateConfig.mutateAsync(mutateWith);
});
expect(put).toHaveBeenCalledWith(`/${pluginId}/configuration`, mutateWith);
expect(queryClient.refetchQueries).toHaveBeenCalledWith(['upload', 'configuration'], {
active: true,
}); });
}); });
test('does handle errors', async () => { expect(result.current.config.isLoading).toBe(true);
const { put } = useFetchClient();
const originalConsoleError = console.error;
console.error = jest.fn();
const toggleNotification = useNotification(); await waitFor(() => expect(result.current.config.isLoading).toBe(false));
put.mockRejectedValueOnce(new Error('Jest mock error'));
const {
result: { current },
} = await setup();
const { mutateConfig } = current;
const mutateWith = {};
try {
await act(async () => {
await mutateConfig.mutateAsync(mutateWith);
});
} catch {
expect(toggleNotification).toBeCalledWith({
type: 'warning',
message: { id: 'notification.error' },
});
}
console.error = originalConsoleError;
}); });
}); });
}); });

View File

@ -1,123 +1,58 @@
import React from 'react'; import { renderHook, screen, server, waitFor } from '@tests/utils';
import { rest } from 'msw';
import { lightTheme, ThemeProvider, useNotifyAT } from '@strapi/design-system';
import { NotificationsProvider, useFetchClient, useNotification } from '@strapi/helper-plugin';
import { act, renderHook, waitFor } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { useFolder } from '../useFolder'; import { useFolder } from '../useFolder';
const notifyStatusMock = jest.fn();
jest.mock('@strapi/design-system', () => ({
...jest.requireActual('@strapi/design-system'),
useNotifyAT: () => ({
notifyStatus: notifyStatusMock,
}),
}));
const notificationStatusMock = jest.fn();
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: () => notificationStatusMock,
useFetchClient: jest.fn().mockReturnValue({
get: jest.fn().mockResolvedValue({
data: {
id: 1,
},
}),
}),
}));
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// eslint-disable-next-line react/prop-types
function ComponentFixture({ children }) {
return (
<Router>
<Route>
<QueryClientProvider client={client}>
<ThemeProvider theme={lightTheme}>
<NotificationsProvider>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</NotificationsProvider>
</ThemeProvider>
</QueryClientProvider>
</Route>
</Router>
);
}
function setup(...args) {
return new Promise((resolve) => {
act(() => {
resolve(renderHook(() => useFolder(...args), { wrapper: ComponentFixture }));
});
});
}
describe('useFolder', () => { describe('useFolder', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('fetches data from the right URL if no query param was set', async () => { test('fetches data from the right URL if no query param was set', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useFolder(1));
const { result } = await setup(1, {});
await waitFor(() => result.current.isSuccess); await waitFor(() => expect(result.current.isLoading).toBe(false));
await waitFor(() => expect(result.current.data).toMatchInlineSnapshot(`
expect(get).toBeCalledWith('/upload/folders/1', { {
params: { "children": {
populate: { "count": 2,
parent: {
populate: {
parent: '*',
}, },
"createdAt": "2023-06-26T12:48:54.054Z",
"files": {
"count": 0,
}, },
}, "id": 1,
}, "name": "test",
}) "parent": null,
); "path": "/1",
"pathId": 1,
"updatedAt": "2023-06-26T12:48:54.054Z",
}
`);
}); });
test('it does not fetch, if enabled is set to false', async () => { test('it does not fetch, if enabled is set to false', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useFolder(1, { enabled: false }));
const { result } = await setup(1, { enabled: false });
await waitFor(() => result.current.isSuccess); await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(get).toBeCalledTimes(0); expect(result.current.data).toBe(undefined);
}); });
test('calls toggleNotification in case of error', async () => { test('calls toggleNotification in case of error', async () => {
const { get } = useFetchClient();
const originalConsoleError = console.error; const originalConsoleError = console.error;
console.error = jest.fn(); console.error = jest.fn();
get.mockRejectedValueOnce(new Error('Jest mock error')); server.use(rest.get('/upload/folders/:id', (req, res, ctx) => res(ctx.status(500))));
const { notifyStatus } = useNotifyAT(); const { result } = renderHook(() => useFolder(1));
const toggleNotification = useNotification();
const { result } = await setup(1, {});
await waitFor(() => !result.current.isLoading); await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(toggleNotification).toBeCalled(); expect(screen.getByText('Not found')).toBeInTheDocument();
expect(notifyStatus).not.toBeCalled();
console.error = originalConsoleError; console.error = originalConsoleError;
server.restoreHandlers();
}); });
}); });

View File

@ -1,223 +1,138 @@
import React from 'react'; import { renderHook, waitFor, screen, server } from '@tests/utils';
import { rest } from 'msw';
import { lightTheme, ThemeProvider, useNotifyAT } from '@strapi/design-system';
import { NotificationsProvider, useFetchClient, useNotification } from '@strapi/helper-plugin';
import { act, renderHook, waitFor } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { useFolders } from '../useFolders'; import { useFolders } from '../useFolders';
const notifyStatusMock = jest.fn();
jest.mock('@strapi/design-system', () => ({
...jest.requireActual('@strapi/design-system'),
useNotifyAT: () => ({
notifyStatus: notifyStatusMock,
}),
}));
const notificationStatusMock = jest.fn();
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: () => notificationStatusMock,
useFetchClient: jest.fn().mockReturnValue({
get: jest.fn().mockResolvedValue({
data: {
id: 1,
},
}),
}),
}));
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// eslint-disable-next-line react/prop-types
function ComponentFixture({ children }) {
return (
<Router>
<Route>
<QueryClientProvider client={client}>
<ThemeProvider theme={lightTheme}>
<NotificationsProvider>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</NotificationsProvider>
</ThemeProvider>
</QueryClientProvider>
</Route>
</Router>
);
}
function setup(...args) {
return new Promise((resolve) => {
act(() => {
resolve(renderHook(() => useFolders(...args), { wrapper: ComponentFixture }));
});
});
}
describe('useFolders', () => { describe('useFolders', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('fetches data from the right URL if no query param was set', async () => { test('fetches data from the right URL if no query param was set', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useFolders());
const { result } = await setup({});
await waitFor(() => result.current.isSuccess); await waitFor(() => expect(result.current.isLoading).toBe(false));
const expected = { expect(result.current.data).toMatchInlineSnapshot(`
pagination: { [
pageSize: -1,
},
filters: {
$and: [
{ {
parent: { "children": {
id: { "count": 2,
$null: true,
}, },
"createdAt": "2023-06-26T12:48:54.054Z",
"files": {
"count": 0,
}, },
"id": 1,
"name": "test",
"path": "/1",
"pathId": 1,
"updatedAt": "2023-06-26T12:48:54.054Z",
}, },
], ]
}, `);
};
await waitFor(() =>
expect(get).toBeCalledWith(`/upload/folders`, {
params: expected,
})
);
}); });
test('does not use parent filter in params if _q', async () => { test('does not use parent filter in params if _q', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() =>
useFolders({
await setup({
query: { folder: 5, _q: 'something', filters: { $and: [{ something: 'true' }] } }, query: { folder: 5, _q: 'something', filters: { $and: [{ something: 'true' }] } },
}); })
);
const expected = { await waitFor(() => expect(result.current.isLoading).toBe(false));
filters: {
$and: [ expect(result.current.data).toMatchInlineSnapshot(`
[
{ {
something: 'true', "children": {
"count": 2,
}, },
], "createdAt": "2023-06-26T12:48:54.054Z",
"files": {
"count": 0,
}, },
pagination: { "id": 1,
pageSize: -1, "name": "something",
"path": "/1",
"pathId": 1,
"updatedAt": "2023-06-26T12:48:54.054Z",
}, },
_q: 'something', ]
}; `);
expect(get).toBeCalledWith(`/upload/folders`, { expect(result.current.data[0].name).toBe('something');
params: expected,
});
}); });
test('fetches data from the right URL if a query param was set', async () => { test('fetches data from the right URL if a query param was set', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useFolders({ query: { folder: 1 } }));
const { result } = await setup({ query: { folder: 1 } });
await waitFor(() => result.current.isSuccess); await waitFor(() => expect(result.current.isLoading).toBe(false));
const expected = { expect(result.current.data).toMatchInlineSnapshot(`
pagination: { [
pageSize: -1,
},
filters: {
$and: [
{ {
parent: { "children": {
id: 1, "count": 0,
}, },
"createdAt": "2023-06-26T12:49:31.354Z",
"files": {
"count": 3,
}, },
], "id": 3,
}, "name": "2022",
}; "path": "/1/3",
"pathId": 3,
expect(get).toBeCalledWith(`/upload/folders`, { "updatedAt": "2023-06-26T12:49:31.354Z",
params: expected,
});
});
test('allows to merge filter query params using filters.$and', async () => {
const { get } = useFetchClient();
await setup({
query: { folder: 5, filters: { $and: [{ something: 'true' }] } },
});
const expected = {
filters: {
$and: [
{
something: 'true',
}, },
{ {
parent: { "children": {
id: 5, "count": 0,
}, },
"createdAt": "2023-06-26T12:49:08.466Z",
"files": {
"count": 3,
}, },
], "id": 2,
"name": "2023",
"path": "/1/2",
"pathId": 2,
"updatedAt": "2023-06-26T12:49:08.466Z",
}, },
pagination: { ]
pageSize: -1, `);
},
};
expect(get).toBeCalledWith(`/upload/folders`, { result.current.data.forEach((folder) => {
params: expected, /**
* We're passing a "current folder" in the query, which means
* any folders returned should include the current folder's ID
* in it's path because this get's the children of current.
*/
expect(folder.path.includes('1')).toBe(true);
}); });
}); });
test('it does not fetch, if enabled is set to false', async () => { test('it does not fetch, if enabled is set to false', async () => {
const { get } = useFetchClient(); const { result } = renderHook(() => useFolders({ enabled: false }));
const { result } = await setup({ enabled: false });
await waitFor(() => result.current.isSuccess); await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(get).toBeCalledTimes(0); expect(result.current.data).toBe(undefined);
});
test('calls notifyStatus in case of success', async () => {
const { notifyStatus } = useNotifyAT();
const toggleNotification = useNotification();
await setup({});
await waitFor(() => {
expect(notifyStatus).toBeCalledWith('The folders have finished loading.');
});
expect(toggleNotification).toBeCalledTimes(0);
}); });
test('calls toggleNotification in case of error', async () => { test('calls toggleNotification in case of error', async () => {
const { get } = useFetchClient();
const originalConsoleError = console.error; const originalConsoleError = console.error;
console.error = jest.fn(); console.error = jest.fn();
get.mockRejectedValueOnce(new Error('Jest mock error')); server.use(rest.get('/upload/folders', (req, res, ctx) => res(ctx.status(500))));
const { notifyStatus } = useNotifyAT(); const { result } = renderHook(() => useFolders());
const toggleNotification = useNotification();
await setup({});
await waitFor(() => expect(toggleNotification).toBeCalled()); await waitFor(() => expect(result.current.isLoading).toBe(false));
await waitFor(() => expect(notifyStatus).not.toBeCalled());
await waitFor(() => expect(screen.getByText('notification.error')).toBeInTheDocument());
console.error = originalConsoleError; console.error = originalConsoleError;
server.restoreHandlers();
}); });
}); });

View File

@ -1,45 +1,7 @@
import React from 'react'; import { act, renderHook, waitFor } from '@tests/utils';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { NotificationsProvider } from '@strapi/helper-plugin';
import { act, renderHook } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import useModalQueryParams from '../useModalQueryParams'; import useModalQueryParams from '../useModalQueryParams';
/**
* TODO: we should set up MSW for these tests
*/
function setup(...args) {
return renderHook(() => useModalQueryParams(...args), {
wrapper({ children }) {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
<MemoryRouter>
<QueryClientProvider client={client}>
<ThemeProvider theme={lightTheme}>
<NotificationsProvider>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</NotificationsProvider>
</ThemeProvider>
</QueryClientProvider>
</MemoryRouter>
);
},
});
}
const FIXTURE_QUERY = { const FIXTURE_QUERY = {
page: 1, page: 1,
sort: 'updatedAt:DESC', sort: 'updatedAt:DESC',
@ -54,17 +16,15 @@ describe('useModalQueryParams', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('setup proper defaults', () => { test('setup proper defaults', async () => {
const { const { result } = renderHook(() => useModalQueryParams());
result: {
current: [{ queryObject, rawQuery }, callbacks],
},
} = setup();
expect(queryObject).toStrictEqual(FIXTURE_QUERY); expect(result.current[0].queryObject).toStrictEqual(FIXTURE_QUERY);
expect(rawQuery).toBe('page=1&sort=updatedAt:DESC&pageSize=10'); expect(result.current[0].rawQuery).toMatchInlineSnapshot(
`"page=1&sort=updatedAt:DESC&pageSize=10"`
);
expect(callbacks).toStrictEqual({ expect(result.current[1]).toStrictEqual({
onChangeFilters: expect.any(Function), onChangeFilters: expect.any(Function),
onChangeFolder: expect.any(Function), onChangeFolder: expect.any(Function),
onChangePage: expect.any(Function), onChangePage: expect.any(Function),
@ -72,29 +32,25 @@ describe('useModalQueryParams', () => {
onChangeSort: expect.any(Function), onChangeSort: expect.any(Function),
onChangeSearch: expect.any(Function), onChangeSearch: expect.any(Function),
}); });
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
}); });
test('set initial state', () => { test('handles initial state', async () => {
const { const { result } = renderHook(() => useModalQueryParams({ state: true }));
result: { current },
} = setup();
expect(current[0].queryObject).toStrictEqual(FIXTURE_QUERY); expect(result.current[0].queryObject).toStrictEqual({
});
test('handles initial state', () => {
const {
result: { current },
} = setup({ state: true });
expect(current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
state: true, state: true,
}); });
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
}); });
test('onChangeFilters', () => { test('onChangeFilters', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangeFilters([{ some: 'thing' }]); result.current[1].onChangeFilters([{ some: 'thing' }]);
@ -102,6 +58,7 @@ describe('useModalQueryParams', () => {
expect(result.current[0].queryObject).toStrictEqual({ expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
pageSize: 20,
filters: { filters: {
...FIXTURE_QUERY.filters, ...FIXTURE_QUERY.filters,
$and: [ $and: [
@ -113,8 +70,10 @@ describe('useModalQueryParams', () => {
}); });
}); });
test('onChangeFolder', () => { test('onChangeFolder', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangeFolder({ id: 1 }, '/1'); result.current[1].onChangeFolder({ id: 1 }, '/1');
@ -122,6 +81,7 @@ describe('useModalQueryParams', () => {
expect(result.current[0].queryObject).toStrictEqual({ expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
pageSize: 20,
folder: { folder: {
id: 1, id: 1,
}, },
@ -129,8 +89,10 @@ describe('useModalQueryParams', () => {
}); });
}); });
test('onChangePage', () => { test('onChangePage', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangePage({ id: 1 }); result.current[1].onChangePage({ id: 1 });
@ -138,14 +100,17 @@ describe('useModalQueryParams', () => {
expect(result.current[0].queryObject).toStrictEqual({ expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
pageSize: 20,
page: { page: {
id: 1, id: 1,
}, },
}); });
}); });
test('onChangePageSize', () => { test('onChangePageSize', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangePageSize(5); result.current[1].onChangePageSize(5);
@ -157,8 +122,10 @@ describe('useModalQueryParams', () => {
}); });
}); });
test('onChangePageSize - converts string to numbers', () => { test('onChangePageSize - converts string to numbers', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangePageSize('5'); result.current[1].onChangePageSize('5');
@ -170,8 +137,10 @@ describe('useModalQueryParams', () => {
}); });
}); });
test('onChangeSort', () => { test('onChangeSort', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangeSort('something:else'); result.current[1].onChangeSort('something:else');
@ -179,12 +148,15 @@ describe('useModalQueryParams', () => {
expect(result.current[0].queryObject).toStrictEqual({ expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
pageSize: 20,
sort: 'something:else', sort: 'something:else',
}); });
}); });
test('onChangeSearch', () => { test('onChangeSearch', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangeSearch('something'); result.current[1].onChangeSearch('something');
@ -192,12 +164,15 @@ describe('useModalQueryParams', () => {
expect(result.current[0].queryObject).toStrictEqual({ expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY, ...FIXTURE_QUERY,
pageSize: 20,
_q: 'something', _q: 'something',
}); });
}); });
test('onChangeSearch - empty string resets all values and removes _q and page', () => { test('onChangeSearch - empty string resets all values and removes _q and page', async () => {
const { result } = setup(); const { result } = renderHook(() => useModalQueryParams());
await waitFor(() => expect(result.current[0].queryObject.pageSize).toBe(20));
act(() => { act(() => {
result.current[1].onChangePage({ id: 1 }); result.current[1].onChangePage({ id: 1 });
@ -211,6 +186,9 @@ describe('useModalQueryParams', () => {
result.current[1].onChangeSearch(''); result.current[1].onChangeSearch('');
}); });
expect(result.current[0].queryObject).toStrictEqual(FIXTURE_QUERY); expect(result.current[0].queryObject).toStrictEqual({
...FIXTURE_QUERY,
pageSize: 20,
});
}); });
}); });

View File

@ -1,5 +1,5 @@
import { useFetchClient, useNotification, useTracking } from '@strapi/helper-plugin'; import { useFetchClient, useNotification, useTracking } from '@strapi/helper-plugin';
import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import pluginId from '../pluginId'; import pluginId from '../pluginId';
@ -7,7 +7,6 @@ const endpoint = `/${pluginId}/configuration`;
const queryKey = [pluginId, 'configuration']; const queryKey = [pluginId, 'configuration'];
export const useConfig = () => { export const useConfig = () => {
const queryClient = useQueryClient();
const { trackUsage } = useTracking(); const { trackUsage } = useTracking();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { get, put } = useFetchClient(); const { get, put } = useFetchClient();
@ -33,10 +32,14 @@ export const useConfig = () => {
} }
); );
const putMutation = useMutation(async (body) => put(endpoint, body), { const putMutation = useMutation(
async (body) => {
await put(endpoint, body);
},
{
onSuccess() { onSuccess() {
trackUsage('didEditMediaLibraryConfig'); trackUsage('didEditMediaLibraryConfig');
queryClient.refetchQueries(queryKey, { active: true }); config.refetch();
}, },
onError() { onError() {
return toggleNotification({ return toggleNotification({
@ -44,7 +47,8 @@ export const useConfig = () => {
message: { id: 'notification.error' }, message: { id: 'notification.error' },
}); });
}, },
}); }
);
return { return {
config, config,

View File

@ -4,13 +4,17 @@ import { useQuery } from 'react-query';
import pluginId from '../pluginId'; import pluginId from '../pluginId';
import { getTrad } from '../utils'; import { getTrad } from '../utils';
export const useFolder = (id, { enabled = true }) => { export const useFolder = (id, { enabled = true } = {}) => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { get } = useFetchClient(); const { get } = useFetchClient();
const fetchFolder = async () => { const { data, error, isLoading } = useQuery(
try { [pluginId, 'folder', id],
const params = { async () => {
const {
data: { data },
} = await get(`/upload/folders/${id}`, {
params: {
populate: { populate: {
parent: { parent: {
populate: { populate: {
@ -18,13 +22,17 @@ export const useFolder = (id, { enabled = true }) => {
}, },
}, },
}, },
}; },
const { });
data: { data },
} = await get(`/upload/folders/${id}`, { params });
return data; return data;
} catch (err) { },
{
retry: false,
enabled,
staleTime: 0,
cacheTime: 0,
onError() {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
message: { message: {
@ -32,17 +40,9 @@ export const useFolder = (id, { enabled = true }) => {
defaultMessage: 'Not found', defaultMessage: 'Not found',
}, },
}); });
},
throw err;
} }
}; );
const { data, error, isLoading } = useQuery([pluginId, 'folder', id], fetchFolder, {
retry: false,
enabled,
staleTime: 0,
cacheTime: 0,
});
return { data, error, isLoading }; return { data, error, isLoading };
}; };

View File

@ -1,3 +1,5 @@
import * as React from 'react';
import { useNotifyAT } from '@strapi/design-system'; import { useNotifyAT } from '@strapi/design-system';
import { useFetchClient, useNotification } from '@strapi/helper-plugin'; import { useFetchClient, useNotification } from '@strapi/helper-plugin';
import { stringify } from 'qs'; import { stringify } from 'qs';
@ -6,7 +8,7 @@ import { useQuery } from 'react-query';
import pluginId from '../pluginId'; import pluginId from '../pluginId';
export const useFolders = ({ enabled = true, query = {} }) => { export const useFolders = ({ enabled = true, query = {} } = {}) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { notifyStatus } = useNotifyAT(); const { notifyStatus } = useNotifyAT();
@ -44,39 +46,38 @@ export const useFolders = ({ enabled = true, query = {} }) => {
}; };
} }
const fetchFolders = async () => { const { data, error, isLoading } = useQuery(
try { [pluginId, 'folders', stringify(params)],
async () => {
const { const {
data: { data }, data: { data },
} = await get('/upload/folders', { params }); } = await get('/upload/folders', { params });
return data;
},
{
enabled,
staleTime: 0,
cacheTime: 0,
onError() {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
},
}
);
React.useEffect(() => {
if (data) {
notifyStatus( notifyStatus(
formatMessage({ formatMessage({
id: 'list.asset.at.finished', id: 'list.asset.at.finished',
defaultMessage: 'The folders have finished loading.', defaultMessage: 'The folders have finished loading.',
}) })
); );
return data;
} catch (err) {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
throw err;
} }
}; }, [data, formatMessage, notifyStatus]);
const { data, error, isLoading } = useQuery(
[pluginId, 'folders', stringify(params)],
fetchFolders,
{
enabled,
staleTime: 0,
cacheTime: 0,
}
);
return { data, error, isLoading }; return { data, error, isLoading };
}; };

View File

@ -1,4 +1,4 @@
import React, { useEffect, useReducer, useRef } from 'react'; import React, { useReducer } from 'react';
import { import {
Box, Box,
@ -22,10 +22,10 @@ import {
useOverlayBlocker, useOverlayBlocker,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { Check } from '@strapi/icons'; import { Check } from '@strapi/icons';
import axios from 'axios';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useMutation, useQuery } from 'react-query';
import { PERMISSIONS } from '../../constants'; import { PERMISSIONS } from '../../constants';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
@ -38,50 +38,50 @@ export const SettingsPage = () => {
const { lockApp, unlockApp } = useOverlayBlocker(); const { lockApp, unlockApp } = useOverlayBlocker();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { get, put } = useFetchClient(); const { get, put } = useFetchClient();
useFocusWhenNavigate(); useFocusWhenNavigate();
const [{ initialData, isLoading, isSubmiting, modifiedData }, dispatch] = useReducer( const [{ initialData, modifiedData }, dispatch] = useReducer(reducer, initialState, init);
reducer,
initialState,
init
);
const isMounted = useRef(true); const { data, isLoading, refetch } = useQuery({
queryKey: ['upload', 'settings'],
useEffect(() => { async queryFn() {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const getData = async () => {
try {
const { const {
data: { data }, data: { data },
} = await get('/upload/settings', { } = await get('/upload/settings');
cancelToken: source.token,
return data;
},
}); });
React.useEffect(() => {
if (data) {
dispatch({ dispatch({
type: 'GET_DATA_SUCCEEDED', type: 'GET_DATA_SUCCEEDED',
data, data,
}); });
} catch (err) {
console.error(err);
} }
}; }, [data]);
if (isMounted.current) {
getData();
}
return () => {
source.cancel('Operation canceled by the user.');
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isSaveButtonDisabled = isEqual(initialData, modifiedData); const isSaveButtonDisabled = isEqual(initialData, modifiedData);
const { mutateAsync, isLoading: isSubmiting } = useMutation({
async mutationFn(body) {
return put('/upload/settings', body);
},
onSuccess() {
refetch();
toggleNotification({
type: 'success',
message: { id: 'notification.form.success.fields' },
});
},
onError(err) {
console.error(err);
},
});
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -91,24 +91,7 @@ export const SettingsPage = () => {
lockApp(); lockApp();
dispatch({ type: 'ON_SUBMIT' }); await mutateAsync(modifiedData);
try {
await put('/upload/settings', modifiedData);
dispatch({
type: 'SUBMIT_SUCCEEDED',
});
toggleNotification({
type: 'success',
message: { id: 'notification.form.success.fields' },
});
} catch (err) {
console.error(err);
dispatch({ type: 'ON_SUBMIT_ERROR' });
}
unlockApp(); unlockApp();
}; };
@ -138,7 +121,6 @@ export const SettingsPage = () => {
primaryAction={ primaryAction={
<Button <Button
disabled={isSaveButtonDisabled} disabled={isSaveButtonDisabled}
data-testid="save-button"
loading={isSubmiting} loading={isSubmiting}
type="submit" type="submit"
startIcon={<Check />} startIcon={<Check />}
@ -175,7 +157,6 @@ export const SettingsPage = () => {
<GridItem col={6} s={12}> <GridItem col={6} s={12}>
<ToggleInput <ToggleInput
aria-label="responsiveDimensions" aria-label="responsiveDimensions"
data-testid="responsiveDimensions"
checked={modifiedData.responsiveDimensions} checked={modifiedData.responsiveDimensions}
hint={formatMessage({ hint={formatMessage({
id: getTrad('settings.form.responsiveDimensions.description'), id: getTrad('settings.form.responsiveDimensions.description'),
@ -205,7 +186,6 @@ export const SettingsPage = () => {
<GridItem col={6} s={12}> <GridItem col={6} s={12}>
<ToggleInput <ToggleInput
aria-label="sizeOptimization" aria-label="sizeOptimization"
data-testid="sizeOptimization"
checked={modifiedData.sizeOptimization} checked={modifiedData.sizeOptimization}
hint={formatMessage({ hint={formatMessage({
id: getTrad('settings.form.sizeOptimization.description'), id: getTrad('settings.form.sizeOptimization.description'),
@ -235,7 +215,6 @@ export const SettingsPage = () => {
<GridItem col={6} s={12}> <GridItem col={6} s={12}>
<ToggleInput <ToggleInput
aria-label="autoOrientation" aria-label="autoOrientation"
data-testid="autoOrientation"
checked={modifiedData.autoOrientation} checked={modifiedData.autoOrientation}
hint={formatMessage({ hint={formatMessage({
id: getTrad('settings.form.autoOrientation.description'), id: getTrad('settings.form.autoOrientation.description'),

View File

@ -2,8 +2,6 @@ import produce from 'immer';
import set from 'lodash/set'; import set from 'lodash/set';
const initialState = { const initialState = {
isLoading: true,
isSubmiting: false,
initialData: { initialData: {
responsiveDimensions: true, responsiveDimensions: true,
sizeOptimization: true, sizeOptimization: true,
@ -22,12 +20,7 @@ const reducer = (state, action) =>
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
produce(state, (drafState) => { produce(state, (drafState) => {
switch (action.type) { switch (action.type) {
case 'CANCEL_CHANGES': {
drafState.modifiedData = state.initialData;
break;
}
case 'GET_DATA_SUCCEEDED': { case 'GET_DATA_SUCCEEDED': {
drafState.isLoading = false;
drafState.initialData = action.data; drafState.initialData = action.data;
drafState.modifiedData = action.data; drafState.modifiedData = action.data;
break; break;
@ -36,19 +29,6 @@ const reducer = (state, action) =>
set(drafState, ['modifiedData', ...action.keys.split('.')], action.value); set(drafState, ['modifiedData', ...action.keys.split('.')], action.value);
break; break;
} }
case 'ON_SUBMIT': {
drafState.isSubmiting = true;
break;
}
case 'SUBMIT_SUCCEEDED': {
drafState.initialData = state.modifiedData;
drafState.isSubmiting = false;
break;
}
case 'ON_SUBMIT_ERROR': {
drafState.isSubmiting = false;
break;
}
default: default:
return state; return state;
} }

View File

@ -1,874 +1,34 @@
/**
*
* Tests for SettingsPage
*
*/
import React from 'react'; import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system'; import { render, waitFor } from '@tests/utils';
import { render, waitFor } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { SettingsPage } from '../index'; import { SettingsPage } from '../index';
import server from './utils/server'; describe('SettingsPage', () => {
it('renders', async () => {
const { getByRole, queryByText } = render(<SettingsPage />);
jest.mock('@strapi/helper-plugin', () => ({ await waitFor(() => expect(queryByText('Loading content.')).not.toBeInTheDocument());
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn(),
useOverlayBlocker: () => ({ lockApp: jest.fn(), unlockApp: jest.fn() }),
useFocusWhenNavigate: jest.fn(),
}));
const App = ( expect(getByRole('heading', { name: 'Media Library' })).toBeInTheDocument();
<ThemeProvider theme={lightTheme}> expect(getByRole('heading', { name: 'Asset management' })).toBeInTheDocument();
<IntlProvider locale="en" messages={{}} textComponent="span">
<SettingsPage />
</IntlProvider>
</ThemeProvider>
);
describe('Upload | SettingsPage', () => { expect(getByRole('button', { name: 'Save' })).toBeInTheDocument();
beforeAll(() => server.listen());
beforeEach(() => { expect(getByRole('checkbox', { name: 'responsiveDimensions' })).toBeInTheDocument();
jest.clearAllMocks(); expect(getByRole('checkbox', { name: 'sizeOptimization' })).toBeInTheDocument();
}); expect(getByRole('checkbox', { name: 'autoOrientation' })).toBeInTheDocument();
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('renders and matches the snapshot', async () => {
const { container, getByText } = render(App);
await waitFor(() =>
expect(
getByText(
'Enabling this option will automatically rotate the image according to EXIF orientation tag.'
)
).toBeInTheDocument()
);
expect(container).toMatchInlineSnapshot(`
.c41 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c6 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
.c11 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #ffffff;
}
.c12 {
font-size: 1rem;
line-height: 1.5;
color: #666687;
}
.c20 {
font-weight: 500;
font-size: 1rem;
line-height: 1.25;
color: #32324d;
}
.c25 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #32324d;
}
.c33 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #666687;
text-transform: uppercase;
}
.c35 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #4945ff;
text-transform: uppercase;
}
.c37 {
font-size: 0.75rem;
line-height: 1.33;
color: #666687;
}
.c39 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #b72b1a;
text-transform: uppercase;
}
.c1 {
background: #f6f6f9;
padding-top: 40px;
padding-right: 56px;
padding-bottom: 40px;
padding-left: 56px;
}
.c3 {
min-width: 0;
}
.c7 {
background: #4945ff;
padding: 8px;
padding-right: 16px;
padding-left: 16px;
border-radius: 4px;
border-color: #4945ff;
border: 1px solid #4945ff;
cursor: pointer;
}
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c15 {
padding-bottom: 56px;
}
.c18 {
background: #ffffff;
padding: 24px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c23 {
max-width: 320px;
}
.c27 {
background: #f6f6f9;
padding: 4px;
border-radius: 4px;
border-style: solid;
border-width: 1px;
border-color: #dcdce4;
position: relative;
cursor: pointer;
}
.c30 {
background: transparent;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
border-radius: 4px;
border-color: #f6f6f9;
border: 1px solid #f6f6f9;
-webkit-flex: 1 1 50%;
-ms-flex: 1 1 50%;
flex: 1 1 50%;
}
.c34 {
background: #ffffff;
padding-right: 12px;
padding-left: 12px;
border-radius: 4px;
border-color: #dcdce4;
border: 1px solid #dcdce4;
-webkit-flex: 1 1 50%;
-ms-flex: 1 1 50%;
flex: 1 1 50%;
}
.c38 {
background: #ffffff;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
border-radius: 4px;
border-color: #dcdce4;
border: 1px solid #dcdce4;
-webkit-flex: 1 1 50%;
-ms-flex: 1 1 50%;
flex: 1 1 50%;
}
.c40 {
background: transparent;
padding-right: 12px;
padding-left: 12px;
border-radius: 4px;
border-color: #f6f6f9;
border: 1px solid #f6f6f9;
-webkit-flex: 1 1 50%;
-ms-flex: 1 1 50%;
flex: 1 1 50%;
}
.c2 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c4 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
.c8 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 8px;
}
.c17 {
-webkit-align-items: stretch;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
gap: 12;
}
.c19 {
-webkit-align-items: stretch;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
gap: 16px;
}
.c24 {
-webkit-align-items: stretch;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
gap: 4px;
}
.c28 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.c31 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c9 {
position: relative;
outline: none;
}
.c9 > svg {
height: 12px;
width: 12px;
}
.c9 > svg > g,
.c9 > svg path {
fill: #ffffff;
}
.c9[aria-disabled='true'] {
pointer-events: none;
}
.c9:after {
-webkit-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
border-radius: 8px;
content: '';
position: absolute;
top: -4px;
bottom: -4px;
left: -4px;
right: -4px;
border: 2px solid transparent;
}
.c9:focus-visible {
outline: none;
}
.c9:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
border: 2px solid #4945ff;
}
.c10 {
height: 2rem;
}
.c10 svg {
height: 0.75rem;
width: auto;
}
.c10[aria-disabled='true'] {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c10[aria-disabled='true'] .c5 {
color: #666687;
}
.c10[aria-disabled='true'] svg > g,
.c10[aria-disabled='true'] svg path {
fill: #666687;
}
.c10[aria-disabled='true']:active {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c10[aria-disabled='true']:active .c5 {
color: #666687;
}
.c10[aria-disabled='true']:active svg > g,
.c10[aria-disabled='true']:active svg path {
fill: #666687;
}
.c10:hover {
border: 1px solid #7b79ff;
background: #7b79ff;
}
.c10:active {
border: 1px solid #4945ff;
background: #4945ff;
}
.c10 svg > g,
.c10 svg path {
fill: #ffffff;
}
.c26 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c21 {
display: grid;
grid-template-columns: repeat(12,1fr);
gap: 24px;
}
.c22 {
grid-column: span 6;
max-width: 100%;
}
.c14 {
display: grid;
grid-template-columns: 1fr;
}
.c16 {
overflow-x: hidden;
}
.c0:focus-visible {
outline: none;
}
.c29 {
outline: none;
box-shadow: 0;
-webkit-transition-property: border-color,box-shadow,fill;
transition-property: border-color,box-shadow,fill;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
}
.c29:focus-within {
border: 1px solid #4945ff;
box-shadow: #4945ff 0px 0px 0px 2px;
}
.c32 {
padding-top: 6px;
padding-bottom: 6px;
}
.c36 {
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
z-index: 0;
width: 100%;
}
@media (max-width:68.75rem) {
.c22 {
grid-column: span 12;
}
}
@media (max-width:34.375rem) {
.c22 {
grid-column: span;
}
}
<div>
<main
aria-labelledby="main-content-title"
class="c0"
id="main-content"
tabindex="-1"
>
<form>
<div
style="height: 0px;"
>
<div
class="c1"
data-strapi-header="true"
>
<div
class="c2"
>
<div
class="c3 c4"
>
<h1
class="c5 c6"
>
Media Library
</h1>
</div>
<button
aria-disabled="true"
class="c7 c8 c9 c10"
data-testid="save-button"
disabled=""
type="submit"
>
<div
aria-hidden="true"
class=""
>
<svg
fill="none"
height="1rem"
viewBox="0 0 24 24"
width="1rem"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.727 2.97a.2.2 0 0 1 .286 0l2.85 2.89a.2.2 0 0 1 0 .28L9.554 20.854a.2.2 0 0 1-.285 0l-9.13-9.243a.2.2 0 0 1 0-.281l2.85-2.892a.2.2 0 0 1 .284 0l6.14 6.209L20.726 2.97Z"
fill="#212134"
/>
</svg>
</div>
<span
class="c5 c11"
>
Save
</span>
</button>
</div>
<p
class="c5 c12"
>
Configure the settings for the Media Library
</p>
</div>
</div>
<div
class="c13"
>
<div
class="c14"
>
<div
class="c15 c16"
>
<div
class="c17"
>
<div
class="c18"
>
<div
class="c19"
>
<div
class="c4"
>
<h2
class="c5 c20"
>
Asset management
</h2>
</div>
<div
class="c21"
>
<div
class="c22"
>
<div
class="c23"
>
<div
class="c24"
>
<div
class="c4"
>
<label
class="c5 c25 c26"
for=":r0:"
>
Responsive friendly upload
</label>
</div>
<div
class="c27 c28 c29"
wrap="wrap"
>
<div
class="c30 c31 c32"
>
<span
class="c5 c33"
>
Off
</span>
</div>
<div
class="c34 c31 c32"
>
<span
class="c5 c35"
>
On
</span>
</div>
<input
aria-describedby=":r0:-hint :r0:-error"
aria-disabled="false"
aria-label="responsiveDimensions"
aria-required="false"
checked=""
class="c36"
data-testid="responsiveDimensions"
id=":r0:"
name="responsiveDimensions"
type="checkbox"
/>
</div>
<p
class="c5 c37"
id=":r0:-hint"
>
Enabling this option will generate multiple formats (small, medium and large) of the uploaded asset.
</p>
</div>
</div>
</div>
<div
class="c22"
>
<div
class="c23"
>
<div
class="c24"
>
<div
class="c4"
>
<label
class="c5 c25 c26"
for=":r2:"
>
Size optimization
</label>
</div>
<div
class="c27 c28 c29"
wrap="wrap"
>
<div
class="c38 c31 c32"
>
<span
class="c5 c39"
>
Off
</span>
</div>
<div
class="c40 c31 c32"
>
<span
class="c5 c33"
>
On
</span>
</div>
<input
aria-describedby=":r2:-hint :r2:-error"
aria-disabled="false"
aria-label="sizeOptimization"
aria-required="false"
class="c36"
data-testid="sizeOptimization"
id=":r2:"
name="sizeOptimization"
type="checkbox"
/>
</div>
<p
class="c5 c37"
id=":r2:-hint"
>
Enabling this option will reduce the image size and slightly reduce its quality.
</p>
</div>
</div>
</div>
<div
class="c22"
>
<div
class="c23"
>
<div
class="c24"
>
<div
class="c4"
>
<label
class="c5 c25 c26"
for=":r4:"
>
Auto orientation
</label>
</div>
<div
class="c27 c28 c29"
wrap="wrap"
>
<div
class="c30 c31 c32"
>
<span
class="c5 c33"
>
Off
</span>
</div>
<div
class="c34 c31 c32"
>
<span
class="c5 c35"
>
On
</span>
</div>
<input
aria-describedby=":r4:-hint :r4:-error"
aria-disabled="false"
aria-label="autoOrientation"
aria-required="false"
checked=""
class="c36"
data-testid="autoOrientation"
id=":r4:"
name="autoOrientation"
type="checkbox"
/>
</div>
<p
class="c5 c37"
id=":r4:-hint"
>
Enabling this option will automatically rotate the image according to EXIF orientation tag.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</main>
<div
class="c41"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>
`);
}); });
it('should display the form correctly with the initial values', async () => { it('should display the form correctly with the initial values', async () => {
const { getByTestId } = render(App); const { getByRole, queryByText } = render(<SettingsPage />);
await waitFor(() => { await waitFor(() => expect(queryByText('Loading content.')).not.toBeInTheDocument());
const responsiveDimension = getByTestId('responsiveDimensions');
const sizeOptimization = getByTestId('sizeOptimization');
const autoOrientation = getByTestId('autoOrientation');
const saveButton = getByTestId('save-button');
expect(responsiveDimension.checked).toBe(true); expect(getByRole('button', { name: 'Save' })).toBeDisabled();
expect(autoOrientation.checked).toBe(true);
expect(sizeOptimization.checked).toBe(false); expect(getByRole('checkbox', { name: 'responsiveDimensions' })).toBeChecked();
expect(saveButton).toBeDisabled(); expect(getByRole('checkbox', { name: 'sizeOptimization' })).toBeChecked();
}); expect(getByRole('checkbox', { name: 'autoOrientation' })).toBeChecked();
}); });
}); });

View File

@ -1,24 +1,6 @@
import reducer from '../reducer'; import reducer from '../reducer';
describe('MEDIA LIBRARY | pages | SettingsPage | reducer', () => { describe('SettingsPage | reducer', () => {
describe('CANCEL_CHANGES', () => {
it('should set the modifiedData with the initialData', () => {
const action = {
type: 'CANCEL_CHANGES',
};
const state = {
initialData: 'test',
modifiedData: 'new test',
};
const expected = {
initialData: 'test',
modifiedData: 'test',
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('GET_DATA_SUCCEEDED', () => { describe('GET_DATA_SUCCEEDED', () => {
it('should set the modifiedData and the initialData correctly', () => { it('should set the modifiedData and the initialData correctly', () => {
const action = { const action = {
@ -27,12 +9,10 @@ describe('MEDIA LIBRARY | pages | SettingsPage | reducer', () => {
}; };
const state = { const state = {
initialData: null, initialData: null,
isLoading: true,
modifiedData: null, modifiedData: null,
}; };
const expected = { const expected = {
initialData: { test: true }, initialData: { test: true },
isLoading: false,
modifiedData: { test: true }, modifiedData: { test: true },
}; };
@ -51,40 +31,12 @@ describe('MEDIA LIBRARY | pages | SettingsPage | reducer', () => {
initialData: { initialData: {
responsiveDimensions: true, responsiveDimensions: true,
}, },
isLoading: false,
modifiedData: { modifiedData: {
responsiveDimensions: true, responsiveDimensions: true,
}, },
}; };
const expected = { const expected = {
initialData: { responsiveDimensions: true }, initialData: { responsiveDimensions: true },
isLoading: false,
modifiedData: { responsiveDimensions: false },
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('SUBMIT_SUCCEEDED', () => {
it('should set the initialData with the modifiedData correctly', () => {
const action = {
type: 'SUBMIT_SUCCEEDED',
};
const state = {
initialData: {
responsiveDimensions: true,
},
isLoading: false,
isSubmiting: true,
modifiedData: {
responsiveDimensions: false,
},
};
const expected = {
initialData: { responsiveDimensions: false },
isLoading: false,
isSubmiting: false,
modifiedData: { responsiveDimensions: false }, modifiedData: { responsiveDimensions: false },
}; };

View File

@ -1,17 +0,0 @@
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const handlers = [
rest.get('*/settings', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
data: { autoOrientation: true, responsiveDimensions: true, sizeOptimization: false },
})
);
}),
];
const server = setupServer(...handlers);
export default server;

View File

@ -1,28 +1,6 @@
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { downloadFile } from '../downloadFile'; import { downloadFile } from '../downloadFile';
const server = setupServer( describe('downloadFile', () => {
rest.get('*/some/file', async (req, res, ctx) => {
const file = new File([new Blob(['1'.repeat(1024 * 1024 + 1)])], 'image.png', {
type: 'image/png',
});
const buffer = await new Response(file).arrayBuffer();
return res(ctx.set('Content-Type', 'image/png'), ctx.body(buffer));
})
);
describe('Upload | utils | downloadFile', () => {
beforeAll(() => {
server.listen();
});
afterAll(() => {
server.close();
});
test('Download target as blob', async () => { test('Download target as blob', async () => {
const setAttributeSpy = jest.fn(); const setAttributeSpy = jest.fn();
const clickSpy = jest.fn(); const clickSpy = jest.fn();

View File

@ -0,0 +1,192 @@
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import qs from 'qs';
export const server = setupServer(
...[
rest.get('/upload/configuration', async (req, res, ctx) => {
return res(
ctx.json({
data: {
/**
* we send the pageSize slightly different to defaults because
* in tests we can track that the async functions have finished.
*/
pageSize: 20,
sort: 'updatedAt:DESC',
},
})
);
}),
rest.put('/upload/configuration', async (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.get('/upload/folders/:id', async (req, res, ctx) => {
return res(
ctx.json({
data: {
id: 1,
name: 'test',
pathId: 1,
path: '/1',
createdAt: '2023-06-26T12:48:54.054Z',
updatedAt: '2023-06-26T12:48:54.054Z',
parent: null,
children: {
count: 2,
},
files: {
count: 0,
},
},
})
);
}),
rest.get('/upload/folders', async (req, res, ctx) => {
const query = qs.parse(req.url.search.slice(1));
if (query._q) {
return res(
ctx.json({
data: [
{
createdAt: '2023-06-26T12:48:54.054Z',
id: 1,
name: query._q,
pathId: 1,
path: '/1',
updatedAt: '2023-06-26T12:48:54.054Z',
children: {
count: 2,
},
files: {
count: 0,
},
},
],
})
);
}
if (Array.isArray(query.filters?.$and)) {
const [{ parent }] = query.filters.$and;
if (parent.id === '1') {
return res(
ctx.json({
data: [
{
createdAt: '2023-06-26T12:49:31.354Z',
id: 3,
name: '2022',
pathId: 3,
path: '/1/3',
updatedAt: '2023-06-26T12:49:31.354Z',
children: {
count: 0,
},
files: {
count: 3,
},
},
{
createdAt: '2023-06-26T12:49:08.466Z',
id: 2,
name: '2023',
pathId: 2,
path: '/1/2',
updatedAt: '2023-06-26T12:49:08.466Z',
children: {
count: 0,
},
files: {
count: 3,
},
},
],
})
);
}
}
return res(
ctx.json({
data: [
{
createdAt: '2023-06-26T12:48:54.054Z',
id: 1,
name: 'test',
pathId: 1,
path: '/1',
updatedAt: '2023-06-26T12:48:54.054Z',
children: {
count: 2,
},
files: {
count: 0,
},
},
],
})
);
}),
rest.get('*/some/file', async (req, res, ctx) => {
const file = new File([new Blob(['1'.repeat(1024 * 1024 + 1)])], 'image.png', {
type: 'image/png',
});
const buffer = await new Response(file).arrayBuffer();
return res(ctx.set('Content-Type', 'image/png'), ctx.body(buffer));
}),
rest.get('/upload/settings', async (req, res, ctx) => {
return res(
ctx.json({
data: {
sizeOptimization: true,
responsiveDimensions: true,
autoOrientation: true,
},
})
);
}),
rest.get('/upload/folder-structure', (req, res, ctx) => {
return res(
ctx.json({
data: [
{
id: 1,
name: 'test',
children: [
{
id: 3,
name: '2022',
children: [],
},
{
id: 2,
name: '2023',
children: [],
},
],
},
],
})
);
}),
rest.get('*/an-image.png', (req, res, ctx) =>
res(ctx.set('Content-Type', 'image/png'), ctx.body())
),
rest.get('*/a-pdf.pdf', (req, res, ctx) =>
res(ctx.set('Content-Type', 'application/pdf'), ctx.body())
),
rest.get('*/a-video.mp4', (req, res, ctx) =>
res(ctx.set('Content-Type', 'video/mp4'), ctx.body())
),
rest.get('*/not-working-like-cors.lutin', (req, res, ctx) => res(ctx.json({}))),
rest.get('*/some-where-not-existing.jpg', (req, res) => res.networkError('Failed to fetch')),
]
);

View File

@ -0,0 +1,13 @@
import { server } from './server';
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});

View File

@ -0,0 +1,97 @@
import * as React from 'react';
import { fixtures } from '@strapi/admin-test-utils';
import { DesignSystemProvider } from '@strapi/design-system';
import { RBACContext, NotificationsProvider } from '@strapi/helper-plugin';
import {
renderHook as renderHookRTL,
render as renderRTL,
waitFor,
act,
fireEvent,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PropTypes from 'prop-types';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { server } from './server';
const Providers = ({ children, initialEntries }) => {
const rbacContextValue = React.useMemo(
() => ({
allPermissions: fixtures.permissions.allPermissions,
}),
[]
);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
// no more errors on the console for tests
onError() {},
},
},
});
return (
// en is the default locale of the admin app.
<MemoryRouter initialEntries={initialEntries}>
<IntlProvider locale="en" textComponent="span">
<DesignSystemProvider locale="en">
<QueryClientProvider client={queryClient}>
<RBACContext.Provider value={rbacContextValue}>
<NotificationsProvider>{children}</NotificationsProvider>
</RBACContext.Provider>
</QueryClientProvider>
</DesignSystemProvider>
</IntlProvider>
</MemoryRouter>
);
};
Providers.defaultProps = {
initialEntries: undefined,
};
Providers.propTypes = {
children: PropTypes.node.isRequired,
initialEntries: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
};
// eslint-disable-next-line react/jsx-no-useless-fragment
const fallbackWrapper = ({ children }) => <>{children}</>;
const render = (ui, { renderOptions, userEventOptions, initialEntries } = {}) => {
const { wrapper: Wrapper = fallbackWrapper, ...restOptions } = renderOptions ?? {};
return {
...renderRTL(ui, {
wrapper: ({ children }) => (
<Providers initialEntries={initialEntries}>
<Wrapper>{children}</Wrapper>
</Providers>
),
...restOptions,
}),
user: userEvent.setup(userEventOptions),
};
};
const renderHook = (hook, options) => {
const { wrapper: Wrapper = fallbackWrapper, ...restOptions } = options ?? {};
return renderHookRTL(hook, {
wrapper: ({ children }) => (
<Providers>
<Wrapper>{children}</Wrapper>
</Providers>
),
...restOptions,
});
};
export { render, renderHook, waitFor, server, act, fireEvent, screen };

View File

@ -3,4 +3,8 @@
module.exports = { module.exports = {
preset: '../../../jest-preset.front.js', preset: '../../../jest-preset.front.js',
displayName: 'Core upload', displayName: 'Core upload',
moduleNameMapper: {
'^@tests/(.*)$': '<rootDir>/admin/tests/$1',
},
setupFilesAfterEnv: ['./admin/tests/setup.js'],
}; };

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@tests/*": ["./admin/tests/*"]
}
}
}