diff --git a/packages/x-data-grid/src/components/panel/filterPanel/GridFilterInputDate.tsx b/packages/x-data-grid/src/components/panel/filterPanel/GridFilterInputDate.tsx index a613c573a082..528af026ff58 100644 --- a/packages/x-data-grid/src/components/panel/filterPanel/GridFilterInputDate.tsx +++ b/packages/x-data-grid/src/components/panel/filterPanel/GridFilterInputDate.tsx @@ -26,6 +26,9 @@ function convertFilterItemValueToInputValue( return ''; } const dateCopy = new Date(itemValue); + if (Number.isNaN(dateCopy.getTime())) { + return ''; + } // The date picker expects the date to be in the local timezone. // But .toISOString() converts it to UTC with zero offset. // So we need to subtract the timezone offset. @@ -69,7 +72,8 @@ function GridFilterInputDate(props: GridFilterInputDateProps) { setIsApplying(true); filterTimeout.start(rootProps.filterDebounceMs, () => { - applyValue({ ...item, value: new Date(value) }); + const date = new Date(value); + applyValue({ ...item, value: Number.isNaN(date.getTime()) ? undefined : date }); setIsApplying(false); }); }, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index e1f098e05799..98e161d10f2c 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -1,40 +1,8 @@ -import * as React from 'react'; import { expect } from 'chai'; -import { createRenderer, ErrorBoundary } from '@mui-internal/test-utils'; -import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { TreeItem } from '@mui/x-tree-view/TreeItem'; +import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; -describe('useTreeViewItems', () => { - const { render } = createRenderer(); - - it('should throw an error when two items have the same ID (items prop approach)', function test() { - // TODO is this fixed? - if (!/jsdom/.test(window.navigator.userAgent)) { - // can't catch render errors in the browser for unknown reason - // tried try-catch + error boundary + window onError preventDefault - this.skip(); - } - - expect(() => - render( - - - , - ), - ).toErrorDev([ - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'The above error occurred in the component:', - ]); - }); - - it('should throw an error when two items have the same ID (JSX approach)', function test() { +describeTreeView('useTreeViewItems plugin', ({ render, treeViewComponent }) => { + it('should throw an error when two items have the same ID', function test() { // TODO is this fixed? if (!/jsdom/.test(window.navigator.userAgent)) { // can't catch render errors in the browser for unknown reason @@ -42,19 +10,15 @@ describe('useTreeViewItems', () => { this.skip(); } - expect(() => - render( - - - - - - , - ), - ).toErrorDev([ - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'The above error occurred in the component:', - ]); + expect(() => render({ items: [{ id: '1' }, { id: '1' }], withErrorBoundary: true })).toErrorDev( + [ + ...(treeViewComponent === 'SimpleTreeView' + ? ['Encountered two children with the same key'] + : []), + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + `The above error occurred in the component`, + ], + ); }); }); diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts index 97357460b867..697265f3b71b 100644 --- a/test/e2e/index.test.ts +++ b/test/e2e/index.test.ts @@ -10,6 +10,7 @@ import { devices, BrowserContextOptions, BrowserType, + WebError, } from '@playwright/test'; import { pickersTextFieldClasses } from '@mui/x-date-pickers/PickersTextField'; import { pickersSectionListClasses } from '@mui/x-date-pickers/PickersSectionList'; @@ -514,6 +515,29 @@ async function initializeEnvironment( await page.locator('[role="gridcell"][data-field="brand"] input').inputValue(), ).not.to.equal('v'); }); + + // https://github.com/mui/mui-x/issues/12705 + it('should not crash if the date is invalid', async () => { + await renderFixture('DataGrid/KeyboardEditDate'); + + await page.hover('div[role="columnheader"][data-field="birthday"]'); + await page.click( + 'div[role="columnheader"][data-field="birthday"] button[aria-label="Menu"]', + ); + await page.click('"Filter"'); + await page.keyboard.type('08/04/2024', { delay: 10 }); + + let thrownError: Error | null = null; + context.once('weberror', (webError: WebError) => { + thrownError = webError.error(); + console.error(thrownError); + }); + + await page.keyboard.press('Backspace'); + + await sleep(200); + expect(thrownError).to.equal(null); + }); }); describe('', () => { diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index 5dc84c400887..6184bf83eefc 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import createDescribe from '@mui-internal/test-utils/createDescribe'; -import { createRenderer } from '@mui-internal/test-utils'; +import { createRenderer, ErrorBoundary } from '@mui-internal/test-utils'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; @@ -55,233 +55,149 @@ const innerDescribeTreeView = ( }; }; - describe(message, () => { - describe('RichTreeView + TreeItem', () => { - const renderRichTreeView: DescribeTreeViewRenderer = ({ - items: rawItems, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const apiRef = { current: undefined }; - const result = render( - - ({ - ...slotProps?.item, - 'data-testid': ownerState.itemId, - }) as any, - }} - getItemLabel={(item) => item.label ?? item.id} - isItemDisabled={(item) => !!item.disabled} - {...other} - />, - ); + const createRendererForComponentWithItemsProp = ( + TreeViewComponent: typeof RichTreeView, + TreeItemComponent: typeof TreeItem | typeof TreeItem2, + ) => { + const wrappedRenderer: DescribeTreeViewRenderer = ({ + items: rawItems, + withErrorBoundary, + slotProps, + ...other + }) => { + const items = rawItems as readonly DescribeTreeViewItem[]; + const apiRef = { current: undefined }; + + const jsx = ( + + ({ + ...slotProps?.item, + 'data-testid': ownerState.itemId, + }) as any, + }} + getItemLabel={(item) => item.label ?? item.id} + isItemDisabled={(item) => !!item.disabled} + {...other} + /> + ); + + const result = render(withErrorBoundary ? {jsx} : jsx); + + return { + setProps: result.setProps, + apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, + ...getUtils(result), + }; + }; - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; + return wrappedRenderer; + }; + + const createRendererForComponentWithJSXItems = ( + TreeViewComponent: typeof SimpleTreeView, + TreeItemComponent: typeof TreeItem | typeof TreeItem2, + ) => { + const wrappedRenderer: DescribeTreeViewRenderer = ({ + items: rawItems, + withErrorBoundary, + slots, + slotProps, + ...other + }) => { + const items = rawItems as readonly DescribeTreeViewItem[]; + const Item = slots?.item ?? TreeItemComponent; + const apiRef = { current: undefined }; + + const renderItem = (item: DescribeTreeViewItem) => ( + + {item.children?.map(renderItem)} + + ); + + const jsx = ( + + {items.map(renderItem)} + + ); + + const result = render(withErrorBoundary ? {jsx} : jsx); + + return { + setProps: result.setProps, + apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, + ...getUtils(result), }; + }; - testRunner({ render: renderRichTreeView, setup: 'RichTreeView + TreeItem' }); + return wrappedRenderer; + }; + + describe(message, () => { + describe('RichTreeView + TreeItem', () => { + testRunner({ + render: createRendererForComponentWithItemsProp(RichTreeView, TreeItem), + setup: 'RichTreeView + TreeItem', + treeViewComponent: 'RichTreeView', + treeItemComponent: 'TreeItem', + }); }); describe('RichTreeView + TreeItem2', () => { - const renderRichTreeView: DescribeTreeViewRenderer = ({ - items: rawItems, - slots, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const apiRef = { current: undefined }; - const result = render( - - ({ - ...slotProps?.item, - 'data-testid': ownerState.itemId, - }) as any, - }} - getItemLabel={(item) => item.label ?? item.id} - isItemDisabled={(item) => !!item.disabled} - {...other} - />, - ); - - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; - }; - - testRunner({ render: renderRichTreeView, setup: 'RichTreeView + TreeItem2' }); + testRunner({ + render: createRendererForComponentWithItemsProp(RichTreeView, TreeItem2), + setup: 'RichTreeView + TreeItem2', + treeViewComponent: 'RichTreeView', + treeItemComponent: 'TreeItem2', + }); }); describe('RichTreeViewPro + TreeItem', () => { - const renderRichTreeViewPro: DescribeTreeViewRenderer = ({ - items: rawItems, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const apiRef = { current: undefined }; - const result = render( - - ({ - ...slotProps?.item, - 'data-testid': ownerState.itemId, - }) as any, - }} - getItemLabel={(item) => item.label ?? item.id} - isItemDisabled={(item) => !!item.disabled} - {...other} - />, - ); - - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; - }; - - testRunner({ render: renderRichTreeViewPro, setup: 'RichTreeView + TreeItem' }); + testRunner({ + render: createRendererForComponentWithItemsProp(RichTreeViewPro, TreeItem), + setup: 'RichTreeViewPro + TreeItem', + treeViewComponent: 'RichTreeViewPro', + treeItemComponent: 'TreeItem', + }); }); describe('RichTreeViewPro + TreeItem2', () => { - const renderRichTreeViewPro: DescribeTreeViewRenderer = ({ - items: rawItems, - slots, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const apiRef = { current: undefined }; - const result = render( - - ({ - ...slotProps?.item, - 'data-testid': ownerState.itemId, - }) as any, - }} - getItemLabel={(item) => item.label ?? item.id} - isItemDisabled={(item) => !!item.disabled} - {...other} - />, - ); - - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; - }; - - testRunner({ render: renderRichTreeViewPro, setup: 'RichTreeView + TreeItem2' }); + testRunner({ + render: createRendererForComponentWithItemsProp(RichTreeViewPro, TreeItem2), + setup: 'RichTreeViewPro + TreeItem2', + treeViewComponent: 'RichTreeViewPro', + treeItemComponent: 'TreeItem2', + }); }); describe('SimpleTreeView + TreeItem', () => { - const renderSimpleTreeView: DescribeTreeViewRenderer = ({ - items: rawItems, - slots, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const Item = slots?.item ?? TreeItem; - const apiRef = { current: undefined }; - - const renderItem = (item: DescribeTreeViewItem) => ( - - {item.children?.map(renderItem)} - - ); - - const result = render( - - {items.map(renderItem)} - , - ); - - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; - }; - - testRunner({ render: renderSimpleTreeView, setup: 'SimpleTreeView + TreeItem' }); + testRunner({ + render: createRendererForComponentWithJSXItems(SimpleTreeView, TreeItem), + setup: 'SimpleTreeView + TreeItem', + treeViewComponent: 'SimpleTreeView', + treeItemComponent: 'TreeItem', + }); }); describe('SimpleTreeView + TreeItem2', () => { - const renderSimpleTreeView: DescribeTreeViewRenderer = ({ - items: rawItems, - slots, - slotProps, - ...other - }) => { - const items = rawItems as readonly DescribeTreeViewItem[]; - const Item = slots?.item ?? TreeItem2; - const apiRef = { current: undefined }; - - const renderItem = (item: DescribeTreeViewItem) => ( - - {item.children?.map(renderItem)} - - ); - - const result = render( - - {items.map(renderItem)} - , - ); - - return { - setProps: result.setProps, - apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), - }; - }; - - testRunner({ render: renderSimpleTreeView, setup: 'SimpleTreeView + TreeItem2' }); + testRunner({ + render: createRendererForComponentWithJSXItems(SimpleTreeView, TreeItem2), + setup: 'SimpleTreeView + TreeItem2', + treeViewComponent: 'SimpleTreeView', + treeItemComponent: 'TreeItem2', + }); }); }); }; @@ -301,6 +217,8 @@ type DescribeTreeView = { * Describe tests for the Tree View that will be executed with the following setups: * - RichTreeView + TreeItem * - RichTreeView + TreeItem2 + * - RichTreeViewPro + TreeItem + * - RichTreeViewPro + TreeItem2 * - SimpleTreeView + TreeItem * - SimpleTreeView + TreeItem2 * diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index 017aa8906f99..e28e480022a8 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -78,6 +78,10 @@ export type DescribeTreeViewRenderer( params: { items: readonly R[]; + /** + * If `true`, the Tree View will be wrapped with an error boundary. + */ + withErrorBoundary?: boolean; } & Omit, 'slots' | 'slotProps'> & { slots?: MergePluginsProperty & { item?: React.ElementType; @@ -88,13 +92,14 @@ export type DescribeTreeViewRenderer DescribeTreeViewRendererReturnValue; +type TreeViewComponent = 'RichTreeView' | 'RichTreeViewPro' | 'SimpleTreeView'; +type TreeItemComponent = 'TreeItem' | 'TreeItem2'; + interface DescribeTreeViewTestRunnerParams { render: DescribeTreeViewRenderer; - setup: - | 'SimpleTreeView + TreeItem' - | 'SimpleTreeView + TreeItem2' - | 'RichTreeView + TreeItem' - | 'RichTreeView + TreeItem2'; + setup: `${TreeViewComponent} + ${TreeItemComponent}`; + treeViewComponent: TreeViewComponent; + treeItemComponent: TreeItemComponent; } export interface DescribeTreeViewItem {