From 65b8da2210a12bb73e9881b02b50e9d9a542ef0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Suwi=C5=84ski?= Date: Wed, 21 May 2025 12:40:30 +0200 Subject: [PATCH 1/3] ListGuesserProps extends with UseResourceDefinitionOptions to fix missing props --- src/list/ListGuesser.spec.tsx | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/list/ListGuesser.spec.tsx diff --git a/src/list/ListGuesser.spec.tsx b/src/list/ListGuesser.spec.tsx new file mode 100644 index 00000000..bf9e3253 --- /dev/null +++ b/src/list/ListGuesser.spec.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { Resource } from '@api-platform/api-doc-parser'; +import { render } from '@testing-library/react'; +import { AdminContext } from 'react-admin'; +import type { GetListResult, GetManyResult } from 'react-admin'; +import ListGuesser from './ListGuesser'; +import SchemaAnalyzerContext from '../introspection/SchemaAnalyzerContext.js'; +import schemaAnalyzer from '../hydra/schemaAnalyzer.js'; +import type { + ApiPlatformAdminDataProvider, + ApiPlatformAdminRecord, +} from '../types.js'; +import { API_FIELDS_DATA } from '../__fixtures__/parsedData.js'; + +const dataProvider: ApiPlatformAdminDataProvider = { + getList: () => + Promise.resolve({ data: [], total: 0 } as GetListResult), + getMany: () => + Promise.resolve({ data: [] } as GetManyResult), + getManyReference: () => + Promise.resolve({ data: [], total: 0 } as GetManyResult), + update: () => + Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), + updateMany: () => Promise.resolve({ data: [] }), + create: () => + Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), + delete: () => + Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), + deleteMany: () => Promise.resolve({ data: [] }), + getOne: () => + Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), + introspect: () => + Promise.resolve({ + data: { + entrypoint: 'entrypoint', + resources: [ + new Resource('books', '/books', { + fields: API_FIELDS_DATA, + readableFields: API_FIELDS_DATA, + writableFields: API_FIELDS_DATA, + parameters: [], + }), + ], + }, + }), + subscribe: () => Promise.resolve({ data: null }), + unsubscribe: () => Promise.resolve({ data: null }), +}; + +// eslint-disable-next-line tree-shaking/no-side-effects-in-initialization +describe('ListGuesser', () => { + it('passing hasEdit and hasShow props are allowed', async () => { + render( + + + + + , + ); + }); +}); From ef781da9fc31bd86fbed906eb7b220653a85fc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Suwi=C5=84ski?= Date: Thu, 26 Mar 2026 10:46:03 +0100 Subject: [PATCH 2/3] skip validation errors notification handling --- src/list/ListGuesser.spec.tsx | 61 ----------------------------------- src/useOnSubmit.test.tsx | 60 ++++++++++++++++++++++++++++++++-- src/useOnSubmit.ts | 16 ++++----- 3 files changed, 66 insertions(+), 71 deletions(-) delete mode 100644 src/list/ListGuesser.spec.tsx diff --git a/src/list/ListGuesser.spec.tsx b/src/list/ListGuesser.spec.tsx deleted file mode 100644 index bf9e3253..00000000 --- a/src/list/ListGuesser.spec.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import { Resource } from '@api-platform/api-doc-parser'; -import { render } from '@testing-library/react'; -import { AdminContext } from 'react-admin'; -import type { GetListResult, GetManyResult } from 'react-admin'; -import ListGuesser from './ListGuesser'; -import SchemaAnalyzerContext from '../introspection/SchemaAnalyzerContext.js'; -import schemaAnalyzer from '../hydra/schemaAnalyzer.js'; -import type { - ApiPlatformAdminDataProvider, - ApiPlatformAdminRecord, -} from '../types.js'; -import { API_FIELDS_DATA } from '../__fixtures__/parsedData.js'; - -const dataProvider: ApiPlatformAdminDataProvider = { - getList: () => - Promise.resolve({ data: [], total: 0 } as GetListResult), - getMany: () => - Promise.resolve({ data: [] } as GetManyResult), - getManyReference: () => - Promise.resolve({ data: [], total: 0 } as GetManyResult), - update: () => - Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), - updateMany: () => Promise.resolve({ data: [] }), - create: () => - Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), - delete: () => - Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), - deleteMany: () => Promise.resolve({ data: [] }), - getOne: () => - Promise.resolve({ data: { id: 'id' } } as { data: RecordType }), - introspect: () => - Promise.resolve({ - data: { - entrypoint: 'entrypoint', - resources: [ - new Resource('books', '/books', { - fields: API_FIELDS_DATA, - readableFields: API_FIELDS_DATA, - writableFields: API_FIELDS_DATA, - parameters: [], - }), - ], - }, - }), - subscribe: () => Promise.resolve({ data: null }), - unsubscribe: () => Promise.resolve({ data: null }), -}; - -// eslint-disable-next-line tree-shaking/no-side-effects-in-initialization -describe('ListGuesser', () => { - it('passing hasEdit and hasShow props are allowed', async () => { - render( - - - - - , - ); - }); -}); diff --git a/src/useOnSubmit.test.tsx b/src/useOnSubmit.test.tsx index 6f8d4462..7619ce85 100644 --- a/src/useOnSubmit.test.tsx +++ b/src/useOnSubmit.test.tsx @@ -2,8 +2,13 @@ import * as React from 'react'; import { jest } from '@jest/globals'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, waitFor } from '@testing-library/react'; -import type { CreateResult, RaRecord, UpdateResult } from 'react-admin'; -import { DataProviderContext, testDataProvider } from 'react-admin'; +import { + type CreateResult, + DataProviderContext, + type RaRecord, + type UpdateResult, + testDataProvider, +} from 'react-admin'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import useOnSubmit from './useOnSubmit.js'; @@ -23,6 +28,16 @@ const onSubmitProps = { }; jest.mock('./getIdentifierValue.js'); +const notify = jest.fn(); +const reactAdminActual = jest.requireActual('react-admin') as Record< + string, + unknown +>; +jest.mock('react-admin', () => ({ + __esModule: true, + ...reactAdminActual, + useNotify: () => notify, +})); test.each([ { @@ -125,3 +140,44 @@ test.each([ }); }); }); + +test('skip validation errors notification handling', async () => { + notify.mockClear(); + const validationError = { name: 'Book name is required' }; + dataProvider.create = jest.fn(() => Promise.reject(new Error('Validation'))); + + let save; + const Dummy = () => { + const onSubmit = useOnSubmit({ + ...onSubmitProps, + schemaAnalyzer: { + ...onSubmitProps.schemaAnalyzer, + getSubmissionErrors: () => validationError, + }, + }); + save = onSubmit; + return ; + }; + + render( + + + + + } /> + + + + , + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const errors = await save({ author: 'Author 1' }); + + await waitFor(() => { + expect(dataProvider.create).toHaveBeenCalled(); + }); + expect(notify).not.toHaveBeenCalled(); + expect(errors).toEqual(validationError); +}); diff --git a/src/useOnSubmit.ts b/src/useOnSubmit.ts index dbc2a8ef..9b4770bf 100644 --- a/src/useOnSubmit.ts +++ b/src/useOnSubmit.ts @@ -92,11 +92,14 @@ const useOnSubmit = ({ const failure = mutationOptions?.onError ?? ((error: string | Error) => { - let message = 'ra.notification.http_error'; - if (!submissionErrors) { - message = - typeof error === 'string' ? error : error.message || message; + // Notification will be handled by the useNotifyIsFormInvalid hook. + if (submissionErrors) { + return; } + const message = + typeof error === 'string' + ? error + : error.message || 'ra.notification.http_error'; let errorMessage; if (typeof error === 'string') { errorMessage = error; @@ -116,10 +119,7 @@ const useOnSubmit = ({ }, {}, ); - if (submissionErrors) { - return submissionErrors; - } - return {}; + return submissionErrors ?? {}; } }, [ From 3779253c68bccebf76b27936ad56c1de20b737be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Suwi=C5=84ski?= Date: Thu, 26 Mar 2026 12:43:41 +0100 Subject: [PATCH 3/3] notification handling on validation errors --- src/useOnSubmit.test.tsx | 81 +++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/src/useOnSubmit.test.tsx b/src/useOnSubmit.test.tsx index 7619ce85..d2da8af2 100644 --- a/src/useOnSubmit.test.tsx +++ b/src/useOnSubmit.test.tsx @@ -11,7 +11,6 @@ import { } from 'react-admin'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import useOnSubmit from './useOnSubmit.js'; import schemaAnalyzer from './hydra/schemaAnalyzer.js'; import { API_FIELDS_DATA } from './__fixtures__/parsedData.js'; @@ -56,6 +55,7 @@ test.each([ ])( 'Call create with file input ($name)', async (values: Omit) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); let save; const Dummy = () => { const onSubmit = useOnSubmit(onSubmitProps); @@ -108,6 +108,7 @@ test.each([ cover: null, }, ])('Call update without file inputs ($name)', async (values: RaRecord) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); let save; const Dummy = () => { const onSubmit = useOnSubmit(onSubmitProps); @@ -141,43 +142,47 @@ test.each([ }); }); -test('skip validation errors notification handling', async () => { - notify.mockClear(); - const validationError = { name: 'Book name is required' }; - dataProvider.create = jest.fn(() => Promise.reject(new Error('Validation'))); - - let save; - const Dummy = () => { - const onSubmit = useOnSubmit({ - ...onSubmitProps, - schemaAnalyzer: { - ...onSubmitProps.schemaAnalyzer, - getSubmissionErrors: () => validationError, - }, - }); - save = onSubmit; - return ; - }; +test.each([{ name: 'Required' }, null])( + 'notification handling on validation errors (%s)', + async (submissionErrors) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); + notify.mockClear(); + dataProvider.create = jest.fn(() => + Promise.reject(new Error('Service Unavailable')), + ); - render( - - - - - } /> - - - - , - ); + let save; + const Dummy = () => { + const onSubmit = useOnSubmit({ + ...onSubmitProps, + schemaAnalyzer: { + ...onSubmitProps.schemaAnalyzer, + getSubmissionErrors: () => submissionErrors, + }, + }); + save = onSubmit; + return ; + }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const errors = await save({ author: 'Author 1' }); + render( + + + + + } /> + + + + , + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const errors = await save({ author: 'Author 1' }); - await waitFor(() => { - expect(dataProvider.create).toHaveBeenCalled(); - }); - expect(notify).not.toHaveBeenCalled(); - expect(errors).toEqual(validationError); -}); + await waitFor(() => { + expect(dataProvider.create).toHaveBeenCalled(); + }); + (submissionErrors ? expect(notify).not : expect(notify)).toHaveBeenCalled(); + expect(errors).toEqual(submissionErrors ?? {}); + }, +);