Skip to content
5 changes: 5 additions & 0 deletions .changeset/eager-yaks-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/expandable-card': minor
---

Adds `getTitle`, `getDescription`, and `getFlagText` test utils to `ExpandableCard`. [LG-5778](https://jira.mongodb.org/browse/LG-5778)
97 changes: 97 additions & 0 deletions packages/expandable-card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,100 @@ import ExpandableCard from '@leafygreen-ui/expandable-card';
| id | `string` | Unique id for the card | |
| className | `string` | Styling prop | |
| contentClassName | `string` | Styling prop for children | |

## Test Harnesses

### `getTestUtils`

`getTestUtils()` is a util that allows consumers to reliably interact with LG `ExpandableCard` in a product test suite. If the `ExpandableCard` instance cannot be found, an error will be thrown.

### Usage

```tsx
import { getTestUtils } from '@leafygreen-ui/expandable-card/testing';

const utils = getTestUtils(lgId?: `lg-${string}`); // lgId refers to the custom `data-lgid` attribute passed to `ExpandableCard`. It defaults to 'lg-expandable_card' if left empty.
```

#### Single `ExpandableCard` component

```tsx
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ExpandableCard, getTestUtils } from '@leafygreen-ui/expandable-card';

test('expandable card', () => {
render(
<ExpandableCard
title="Lorem Ipsum"
description="Im leafy"
flagText="optional"
darkMode={false}
>
<p>We are all leafy</p>
</ExpandableCard>,
);
const { getExpandableCard, isExpanded, getTitle, getDescription } =
getTestUtils();

expect(getExpandableCard()).toBeInTheDocument();
expect(getTitle()).toHaveTextContent('Lorem Ipsum');
expect(getDescription()).toHaveTextContent('Im leafy');
expect(isExpanded()).toBeFalsy();
});
```

#### Multiple `ExpandableCard` components

When testing multiple `ExpandableCard` components it is recommended to add the custom `data-lgid` attribute to each `ExpandableCard`.

```tsx
import { render } from '@testing-library/react';
import { ExpandableCard } from '@leafygreen-ui/expandable-card';
import { getTestUtils } from '@leafygreen-ui/expandable-card/testing';

...

test('multiple expandable cards', () => {
render(
<>
<ExpandableCard data-lgid="lg-expandable_card-1" title="Card 1 Title" description="Im leafy" flagText="optional" darkMode={false} >
<p>We are all leafy</p>
</ExpandableCard>
<ExpandableCard data-lgid="lg-expandable_card-2" title="Card 2 Title" description="Im not leafy" flagText="boptional" darkMode={false} >
<p>We are all not leafy</p>
</ExpandableCard>
</>
);
const utilsOne = getTestUtils('lg-expandable_card-1');
const utilsTwo = getTestUtils('lg-expandable_card-2');

expect(utilsOne.getTitle()).toHaveTextContent('Card 1 Title');
expect(utilsTwo.getTitle()).toHaveTextContent('Card 2 Title');
```

### Test Utils

```tsx
const {
findExpandableCard,
getExpandableCard,
queryExpandableCard,
getToggle,
isExpanded,
getTitle,
getDescription,
getFlagText,
} = getTestUtils();
```

| Util | Description | Returns |
| --------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `findExpandableCard` | Returns a promise that resolves to the component's root element. | `Promise<HTMLDivElement>` |
| `getExpandableCard` | Returns the component's root element. Will throw if no elements match or if more than one match is found. | `HTMLDivElement` |
| `queryExpandableCard` | Returns the component's root element or `null` if not found. | `HTMLDivElement` \| `null` |
| `getToggle` | Returns the component's toggle button element. | `HTMLButtonElement` |
| `isExpanded` | Returns a boolean indicating whether the card is expanded. | `boolean` |
| `getTitle` | Returns the component's title element. | `HTMLHeadingElement` \| `null` |
| `getDescription` | Returns the component's description element. | `HTMLDivElement` \| `null` |
| `getFlagText` | Returns the component's flag text element. | `HTMLSpanElement` \| `null` |
29 changes: 24 additions & 5 deletions packages/expandable-card/src/ExpandableCard/ExpandableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LeafyGreenProvider, {
} from '@leafygreen-ui/leafygreen-provider';
import { Body, Subtitle } from '@leafygreen-ui/typography';

import { DEFAULT_LGID_ROOT, getLgIds } from '../testing';
import { getLgIds } from '../testing';

import {
cardStyle,
Expand Down Expand Up @@ -43,7 +43,7 @@ const ExpandableCard = ({
id: idProp,
flagText,
contentClassName,
'data-lgid': dataLgId = DEFAULT_LGID_ROOT,
'data-lgid': dataLgId,
...rest
}: ExpandableCardProps) => {
const { darkMode, theme } = useDarkMode(darkModeProp);
Expand Down Expand Up @@ -123,11 +123,30 @@ const ExpandableCard = ({
tabIndex={-1}
>
<span>
<Subtitle className={summaryHeader}>{title}</Subtitle>
{flagText && <span className={flagTextStyle}>{flagText}</span>}
<Subtitle
data-testid={lgIds.title}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been looking into this pattern of adding lgIds to data-testid, and it seems like 3/4ths as many instances of it as passing it into data-lgid. My personal opinion is allowing the users to control what they want data-testid to do while we control data-lgid. What do you think?

CC @stephl3

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree. The pattern of implementing test utils and setting data-testid seems duplicative and confusing. What is the purpose of setting both?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few reasons. This is to support consumers who are testing without a DOM. Our test utils require the use of the DOM, but without a DOM you can still query with data-testid. This is also helpful for components with child components, where consumers can't add data-testid. For example, in Modal, consumers can't add a data-testid to the close button`.

data-lgid={lgIds.title}
className={summaryHeader}
>
{title}
</Subtitle>
{flagText && (
<span
data-testid={lgIds.flagText}
data-lgid={lgIds.flagText}
className={flagTextStyle}
>
{flagText}
</span>
)}
</span>
{description && (
<Body as="div" className={summaryTextThemeStyle[theme]}>
<Body
as="div"
data-testid={lgIds.description}
data-lgid={lgIds.description}
className={summaryTextThemeStyle[theme]}
>
{description}
</Body>
)}
Expand Down
36 changes: 36 additions & 0 deletions packages/expandable-card/src/testing/getTestUtils.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ describe('packages/expandable-card/getTestUtils', () => {
expect(card).toBeInTheDocument();
});
});

describe('getTitle', () => {
test('returns the title element', () => {
renderExpandableCard();
const utils = getTestUtils();
const title = utils.getTitle();
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent(defaultProps.title);
});
});

describe('getDescription', () => {
test('returns the description element', () => {
renderExpandableCard();
const utils = getTestUtils();
const description = utils.getDescription();
expect(description).toBeInTheDocument();
expect(description).toHaveTextContent(defaultProps.description);
});
});

describe('getFlagText', () => {
test('returns the flag text element', () => {
renderExpandableCard();
const utils = getTestUtils();
const flagText = utils.getFlagText();
expect(flagText).toBeInTheDocument();
expect(flagText).toHaveTextContent('optional');
});
});
});

describe('multiple expandable cards', () => {
Expand All @@ -92,10 +122,16 @@ describe('packages/expandable-card/getTestUtils', () => {
// First card should be collapsed by default
expect(utils1.isExpanded()).toBeFalsy();
expect(utils1.getToggle()).toHaveTextContent('Card Title');
expect(utils1.getTitle()).toHaveTextContent('Card Title');
expect(utils1.getDescription()).toHaveTextContent('Card Description');
expect(utils1.getFlagText()).toHaveTextContent('optional');

// Second card was rendered with defaultOpen={true}
expect(utils2.isExpanded()).toBeTruthy();
expect(utils2.getToggle()).toHaveTextContent('Card 2 Title');
expect(utils2.getTitle()).toHaveTextContent('Card 2 Title');
expect(utils2.getDescription()).toHaveTextContent('Card Description');
expect(utils2.getFlagText()).toHaveTextContent('optional');
});

test('can toggle cards independently', () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/expandable-card/src/testing/getTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@ export const getTestUtils = <T extends HTMLDivElement = HTMLDivElement>(
return toggle.getAttribute('aria-expanded') === 'true';
};

const getTitle = () => queryByLgId!<HTMLDivElement>(lgIds.title);

const getDescription = () => queryByLgId!<HTMLDivElement>(lgIds.description);

const getFlagText = () => queryByLgId!<HTMLDivElement>(lgIds.flagText);

return {
findExpandableCard,
getExpandableCard,
getToggle,
isExpanded,
queryExpandableCard,
getTitle,
getDescription,
getFlagText,
};
};
15 changes: 15 additions & 0 deletions packages/expandable-card/src/testing/getTestUtils.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ export interface GetTestUtilsReturnType<
* @returns whether the expandable card is currently expanded.
*/
isExpanded: () => boolean;

/**
* @returns the title element of the expandable card.
*/
getTitle: () => HTMLHeadingElement | null;

/**
* @returns the description element of the expandable card.
*/
getDescription: () => HTMLDivElement | null;

/**
* @returns the flag text element of the expandable card.
*/
getFlagText: () => HTMLSpanElement | null;
}
3 changes: 3 additions & 0 deletions packages/expandable-card/src/testing/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const getLgIds = (root: `lg-${string}` = DEFAULT_LGID_ROOT) => {
return {
root,
toggle: `${root}-toggle`,
title: `${root}-title`,
description: `${root}-description`,
flagText: `${root}-flag-text`,
} as const;
};

Expand Down
Loading