From f3578f8871d78246b6df915575dac1f3c30630c2 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Sat, 12 Sep 2026 00:10:15 +0530 Subject: [PATCH] fix(widget-list): restore controls for erroneous typed items --- .../decap-cms-widget-list/src/ListControl.js | 7 +- .../__tests__/ListControl.erroneous.spec.js | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 packages/decap-cms-widget-list/src/__tests__/ListControl.erroneous.spec.js diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index ef9e1e2314ba..87ca03cfa2de 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -736,16 +736,19 @@ export default class ListControl extends Component { renderErroneousTypedItem(index, item) { const field = this.props.field; const errorMessage = getErrorMessageForTypedFieldAndValue(field, item); - const key = `item-${index}`; + const key = this.state.keys[index]; return ( diff --git a/packages/decap-cms-widget-list/src/__tests__/ListControl.erroneous.spec.js b/packages/decap-cms-widget-list/src/__tests__/ListControl.erroneous.spec.js new file mode 100644 index 000000000000..b9e76a4283fb --- /dev/null +++ b/packages/decap-cms-widget-list/src/__tests__/ListControl.erroneous.spec.js @@ -0,0 +1,80 @@ +import { fireEvent, render } from '@testing-library/react'; +import { fromJS } from 'immutable'; +import { useState } from 'react'; + +import ListControl from '../ListControl'; + +describe('erroneous typed list items', () => { + const field = fromJS({ + name: 'sections', + types: [{ name: 'text', widget: 'object', fields: [{ name: 'body', widget: 'string' }] }], + }); + + function renderList(value, overrides = {}) { + const onChange = jest.fn(); + function ControlledList() { + const [currentValue, setValue] = useState(fromJS(value)); + return ( + { + onChange(nextValue, metadata); + setValue(nextValue); + }} + onChangeObject={jest.fn()} + validate={jest.fn()} + mediaPaths={fromJS({})} + getAsset={jest.fn()} + onOpenMediaLibrary={jest.fn()} + onAddAsset={jest.fn()} + onRemoveInsertedMedia={jest.fn()} + classNameWrapper="list-control" + setActiveStyle={jest.fn()} + setInactiveStyle={jest.fn()} + editorControl={jest.fn()} + resolveWidget={jest.fn()} + onValidateObject={jest.fn()} + clearFieldErrors={jest.fn()} + fieldsErrors={fromJS({})} + entry={fromJS({ path: 'pages/index.md' })} + forID="sections" + t={key => key} + /> + ); + } + const result = render(); + return { ...result, onChange }; + } + + it.each([ + [{ body: 'Missing type' }, "Error: item has no 'type' property"], + [ + { type: 'unknown', body: 'Unknown type' }, + "Error: item has illegal 'type' property: 'unknown'", + ], + ])('allows removing an invalid item: %j', (item, message) => { + const remaining = { body: 'Keep this item' }; + const { getAllByText, onChange } = renderList([item, remaining]); + const row = getAllByText(message)[0].parentElement; + const removeButton = row.querySelector('button'); + + expect(removeButton).toBeInTheDocument(); + fireEvent.click(removeButton); + + expect(onChange).toHaveBeenCalledWith(fromJS([remaining]), undefined); + }); + + it.each([ + [{}, true, true], + [{ allow_remove: false }, false, true], + [{ allow_reorder: false }, true, false], + [{ allow_remove: false, allow_reorder: false }, false, false], + ])('respects the configured controls: %j', (overrides, allowRemove, allowReorder) => { + const { getByText } = renderList([{ body: 'Missing type' }], overrides); + const row = getByText("Error: item has no 'type' property").parentElement; + + expect(Boolean(row.querySelector('button'))).toBe(allowRemove); + expect(Boolean(row.querySelector('[aria-roledescription="sortable"]'))).toBe(allowReorder); + }); +});