diff --git a/src/attribute-editor/__tests__/warnings.test.tsx b/src/attribute-editor/__tests__/warnings.test.tsx new file mode 100644 index 0000000000..0ad4b44e71 --- /dev/null +++ b/src/attribute-editor/__tests__/warnings.test.tsx @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import AttributeEditor from '../../../lib/components/attribute-editor'; + +jest.mock('@cloudscape-design/component-toolkit', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit'), + useContainerQuery: jest.fn().mockImplementation(() => ['m', () => {}]), +})); + +let consoleWarnSpy: jest.SpyInstance; +beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); +}); +afterEach(() => { + consoleWarnSpy?.mockRestore(); +}); + +/** + * This test suite is in a separate file, because it needs a clean messageCache (inside `warnOnce()`). + * Otherwise, warnings would not appear at the expected time in the test, because they have been issued before. + */ +describe('AttributeEditor component', () => { + test('warns when a definition has no label', () => { + render( + 'key' }]} + items={[{}]} + gridLayout={[{ rows: [[1]] }]} + /> + ); + expect(console.warn).not.toHaveBeenCalled(); + + render( + 'key' }, { control: () => 'value' }]} + items={[{}]} + gridLayout={[{ rows: [[1, 1]] }]} + /> + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + '[AwsUi] [AttributeEditor] A `label` should be provided for each field definition. It is used as `aria-label` for accessibility.' + ); + }); +}); diff --git a/src/attribute-editor/internal.tsx b/src/attribute-editor/internal.tsx index 7f9acd1006..24f3229736 100644 --- a/src/attribute-editor/internal.tsx +++ b/src/attribute-editor/internal.tsx @@ -3,7 +3,7 @@ import React, { useImperativeHandle, useRef, useState } from 'react'; import clsx from 'clsx'; -import { useMergeRefs, useUniqueId } from '@cloudscape-design/component-toolkit/internal'; +import { useMergeRefs, useUniqueId, warnOnce } from '@cloudscape-design/component-toolkit/internal'; import { ButtonProps } from '../button/interfaces'; import { InternalButton } from '../button/internal'; @@ -92,6 +92,16 @@ const InternalAttributeEditor = React.forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, i18nStrings?.itemRemovedAriaLive]); + for (const def of definition) { + if (def && !def.label) { + warnOnce( + 'AttributeEditor', + 'A `label` should be provided for each field definition. It is used as `aria-label` for accessibility.' + ); + break; + } + } + if (!gridLayout) { gridLayout = gridDefaults[definition.length]; if (!gridLayout) {