diff --git a/src/cards/__tests__/cards.test.tsx b/src/cards/__tests__/cards.test.tsx index cb52a5102c..5d8b4bae0b 100644 --- a/src/cards/__tests__/cards.test.tsx +++ b/src/cards/__tests__/cards.test.tsx @@ -27,6 +27,7 @@ function findFooterPagination(wrapper: CardsWrapper): PaginationWrapper | null { const cardDefinition: CardsProps.CardDefinition = { header: item => `Header ${item.name}`, + testId: item => `card-${item.id}`, sections: [ { id: 'description', @@ -59,14 +60,19 @@ describe('Cards', () => { // index is 0-based const getCard = (index: number) => wrapper.findItems()[index]; - const getCardHeader = (index: number) => getCard(index)?.findCardHeader()?.getElement(); + const getCardByTestId = (testId: string) => wrapper.findItemByTestId(testId)!; + + const getCardHeader = (index: number) => wrapper.findItems()[index]?.findCardHeader()?.getElement(); // index is 0-based - const getCardsSections = (index: number) => getCard(index).findSections(); + const getCardsSections = (index: number) => wrapper.findItems()[index].findSections(); // cardIndex and sectionIndex are 0-based const getCardSection = (cardIndex: number, sectionIndex: number) => getCardsSections(cardIndex)[sectionIndex]; + const getCardSectionByTestId = (cardIndex: number, sectionTestId: string) => + getCard(cardIndex)!.findSectionByTestId(sectionTestId)!; + const getCardSectionHeader = (cardIndex: number, sectionIndex: number) => getCardSection(cardIndex, sectionIndex)?.findSectionHeader()?.getElement(); @@ -85,8 +91,7 @@ describe('Cards', () => { wrapper = renderCards( cardDefinition={cardDefinition} items={defaultItems} selectionType="single" /> ).wrapper; - - const cardsOrderedList = getCard(0).getElement().parentElement; + const cardsOrderedList = getCardByTestId('card-1').getElement().parentElement; expect(cardsOrderedList).toHaveAttribute('role', 'group'); }); @@ -142,6 +147,7 @@ describe('Cards', () => { sections: [ { content: item => item.name, + testId: item => `card-${item.id}-content-section`, }, ], }} @@ -151,7 +157,8 @@ describe('Cards', () => { defaultItems.forEach((item, idx) => { expect(getCardsSections(idx)).toHaveLength(1); - expect(getCardSection(idx, 0).findSectionHeader()).toBe(null); + const sectionTestId = `card-${idx + 1}-content-section`; + expect(getCardSectionByTestId(idx, sectionTestId).findSectionHeader()).toBe(null); expect(getCardSectionContent(idx, 0)).toHaveTextContent(item.name); }); }); @@ -176,6 +183,36 @@ describe('Cards', () => { expect(getCardSection(idx, 0)?.findContent()).toBe(null); }); }); + + it('assigns test id attributes to the cards and sections', () => { + const { wrapper } = renderCards( + + cardDefinition={{ + testId: item => `${item.name}-${item.id}`, + sections: [ + { + testId: item => `${item.name}-${item.id}-id-section`, + header: 'id', + }, + { + testId: item => `${item.name}-${item.id}-name-section`, + header: 'name', + }, + ], + }} + items={defaultItems} + /> + ); + + const itemTestIds = wrapper.findItems().map(item => item.getElement()!.getAttribute('data-testid')); + expect(itemTestIds).toEqual(['Apples-1', 'Oranges-2', 'Bananas-3']); + + const secondItemSectionTestIds = wrapper + .findItems()[1] + .findSections() + .map(section => section.getElement()!.getAttribute('data-testid')); + expect(secondItemSectionTestIds).toEqual(['Oranges-2-id-section', 'Oranges-2-name-section']); + }); }); describe('header region', () => { @@ -308,10 +345,85 @@ describe('Cards', () => { cardDefinition={cardDefinition} selectionType="multi" items={defaultItems} /> )); - expect(getCard(0).findSelectionArea()!.getElement()).toHaveAttribute( + expect(getCardByTestId('card-1').findSelectionArea()!.getElement()).toHaveAttribute( 'aria-label', expect.stringContaining('Custom label') ); }); }); + + describe('test utils', () => { + it('findItemByTestId returns the card by test id', () => { + const { wrapper } = renderCards( + + cardDefinition={{ + testId: item => `card-${item.id}`, + sections: [ + { + content: item => item.name, + }, + ], + }} + items={defaultItems} + /> + ); + + expect(wrapper.findItemByTestId('card-2')!.getElement()).toHaveTextContent('Orange'); + }); + + it('findItemByTestId returns the card even if test id contains quotes', () => { + const { wrapper } = renderCards( + + cardDefinition={{ + testId: item => `card-"${item.id}"`, + }} + items={defaultItems} + /> + ); + + expect(wrapper.findItemByTestId('card-"2"')).toBeTruthy(); + }); + + it('findSectionByTestId returns the section by test id', () => { + const { wrapper } = renderCards( + + cardDefinition={{ + sections: [ + { + content: item => `Item ID: ${item.id}`, + testId: item => `card-${item.id}-id`, + }, + { + content: item => `Item Name: ${item.name}`, + testId: item => `card-${item.id}-name`, + }, + ], + }} + items={defaultItems} + /> + ); + + const firstItem = wrapper.findItems()[0]!; + expect(firstItem.findSectionByTestId('card-1-id')!.getElement()).toHaveTextContent('Item ID: 1'); + expect(firstItem.findSectionByTestId('card-1-name')!.getElement()).toHaveTextContent('Item Name: Apples'); + }); + + it('findSectionByTestId returns the section even if test id contains quotes', () => { + const { wrapper } = renderCards( + + cardDefinition={{ + sections: [ + { + testId: item => `card-section-"${item.id}"`, + }, + ], + }} + items={defaultItems} + /> + ); + + const firstItem = wrapper.findItems()[0]!; + expect(firstItem.findSectionByTestId('card-section-"1"')).toBeTruthy(); + }); + }); }); diff --git a/src/cards/index.tsx b/src/cards/index.tsx index 5dbecb0433..67cbcdd923 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -271,6 +271,7 @@ const CardsList = ({ [styles['card-selected']]: selectable && isItemSelected(item), })} key={getItemKey(trackBy, item, index)} + data-testid={cardDefinition.testId?.(item)} onFocus={onFocus} {...(focusMarkers && focusMarkers.item)} role={listItemRole} @@ -301,8 +302,13 @@ const CardsList = ({ )} - {visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => ( -
+ {visibleSectionsDefinition.map(({ width = 100, header, content, id, testId }, index) => ( +
{header ?
{header}
: ''} {content ?
{content(item)}
: ''}
diff --git a/src/cards/interfaces.tsx b/src/cards/interfaces.tsx index 25b0066f3f..eb786fb2c0 100644 --- a/src/cards/interfaces.tsx +++ b/src/cards/interfaces.tsx @@ -223,6 +223,15 @@ export namespace CardsProps { export interface CardDefinition { header?(item: T): React.ReactNode; sections?: ReadonlyArray>; + + /** + * Returns the test ID for each card item. + * Returned value is assigned to the `data-testid` attribute of the card's root element. + * + * @param {T} item Single item from the specified `items` array. + * @returns {string} Test id to be assigned to the corresponding card. + */ + testId?(item: T): string; } export interface SectionDefinition { @@ -230,6 +239,15 @@ export namespace CardsProps { header?: React.ReactNode; content?(item: T): React.ReactNode; width?: number; + + /** + * Returns the test ID for each section item. + * Returned value is assigned to the `data-testid` attribute of the section's root element. + * + * @param {T} item Single item from the specified `items` array. + * @returns {string} Test id to be assigned to the corresponding section. + */ + testId?(item: T): string; } export interface CardsLayout { diff --git a/src/test-utils/dom/cards/index.ts b/src/test-utils/dom/cards/index.ts index 71b1afcb0b..3afc8b0936 100644 --- a/src/test-utils/dom/cards/index.ts +++ b/src/test-utils/dom/cards/index.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; +import { escapeSelector } from '@cloudscape-design/test-utils-core/utils'; import CollectionPreferencesWrapper from '../collection-preferences'; import ContainerWrapper from '../container'; @@ -30,6 +31,19 @@ export class CardWrapper extends ComponentWrapper { return this.findAllByClassName(styles.section).map(c => new CardSectionWrapper(c.getElement())); } + /** + * Returns the wrapper of the first card section that matches the specified test ID. + * Looks for the `data-testid` attribute that is assigned via `sections.testId` prop. + * If no matching card section is found, returns `null`. + * + * @param {string} testId + * @returns {CardSectionWrapper | null} + */ + findSectionByTestId(testId: string): CardSectionWrapper | null { + const escapedTestId = escapeSelector(testId); + return this.findComponent(`.${styles.section}[data-testid="${escapedTestId}"]`, CardSectionWrapper); + } + findCardHeader(): ElementWrapper | null { return this.findByClassName(styles['card-header-inner']); } @@ -48,6 +62,19 @@ export default class CardsWrapper extends ComponentWrapper { return this.findAllByClassName(styles.card).map(c => new CardWrapper(c.getElement())); } + /** + * Returns the wrapper of the first card that matches the specified test ID. + * Looks for the `data-testid` attribute that is assigned via `cardDefinition.testId` prop. + * If no matching card is found, returns `null`. + * + * @param {string} testId + * @returns {CardWrapper | null} + */ + findItemByTestId(testId: string): CardWrapper | null { + const escapedTestId = escapeSelector(testId); + return this.findComponent(`.${styles.card}[data-testid="${escapedTestId}"]`, CardWrapper); + } + findSelectedItems(): Array { return this.findAllByClassName(styles['card-selected']).map(c => new CardWrapper(c.getElement())); }