diff --git a/.changeset/gorgeous-buckets-kiss.md b/.changeset/gorgeous-buckets-kiss.md new file mode 100644 index 0000000000..dc16bee649 --- /dev/null +++ b/.changeset/gorgeous-buckets-kiss.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/table': patch +--- + +Updates internal test suite diff --git a/.changeset/green-hotels-pull.md b/.changeset/green-hotels-pull.md new file mode 100644 index 0000000000..e875927d1e --- /dev/null +++ b/.changeset/green-hotels-pull.md @@ -0,0 +1,10 @@ +--- +'@leafygreen-ui/button': minor +--- + + +- Exports `getTestUtils`, a util to reliably interact with `LG Button` in a product test suite. For more details, check out the [README](https://github.com/mongodb/leafygreen-ui/tree/main/packages/button#test-harnesses) +- Exports the constant, `LGIDS_BUTTON`, which stores `data-lgid` values. + + + diff --git a/.changeset/sour-crabs-move.md b/.changeset/sour-crabs-move.md new file mode 100644 index 0000000000..7a98df5176 --- /dev/null +++ b/.changeset/sour-crabs-move.md @@ -0,0 +1,8 @@ +--- +'@leafygreen-ui/checkbox': minor +--- + +- Exports `getTestUtils`, a util to reliably interact with `LG Checkbox` in a product test suite. For more details, check out the [README](https://github.com/mongodb/leafygreen-ui/tree/main/packages/checkbox#test-harnesses) +- Exports the constant, `LGIDS_CHECKBOX`, which stores `data-lgid` values. +- Leverages the `'aria-label'` prop when passed + diff --git a/packages/button/README.md b/packages/button/README.md index f65c1ced96..34c2f5a58d 100644 --- a/packages/button/README.md +++ b/packages/button/README.md @@ -99,3 +99,73 @@ npm install @leafygreen-ui/button | ... | native attributes of component passed to as prop | Any other properties will be spread on the root element | | _Note: In order to make this Component act as a submit button, the recommended approach is to pass `submit` as the `type` prop. Note it is also valid to pass `input` to the `as` prop, and the button's content's to the `value` prop -- in this case, do not supply children to the component._ + +# Test Harnesses + +## getTestUtils() + +`getTestUtils()` is a util that allows consumers to reliably interact with `LG Button` in a product test suite. If the `Button` component cannot be found, an error will be thrown. + +### Usage + +```tsx +import Button, { getTestUtils } from '@leafygreen-ui/button'; + +const utils = getTestUtils(lgId?: string); // lgId refers to the custom `data-lgid` attribute passed to `Button`. It defaults to 'lg-button' if left empty. +``` + +#### Single `Button` + +```tsx +import { render } from '@testing-library/react'; +import Button, { getTestUtils } from '@leafygreen-ui/button'; + +... + +test('button', () => { + render(); + const { getButton } = getTestUtils(); + + expect(getButton()).toBeInTheDocument(); +}); +``` + +#### Multiple `Button` components + +When testing multiple `Button` components it is recommended to add the custom `data-lgid` attribute to each `Button`. + +```tsx +import { render } from '@testing-library/react'; +import Button, { getTestUtils } from '@leafygreen-ui/button'; + +... + +test('button', () => { + render( + <> + + + , + ); + const utilsOne = getTestUtils('button-1'); // data-lgid + const utilsTwo = getTestUtils('button-2'); // data-lgid + // First Button + expect(utilsOne.getButton()).toBeInTheDocument(); + expect(utilsOne.isDisabled()).toBe(false); + + // Second Button + expect(utilsTwo.getButton()).toBeInTheDocument(); + expect(utilsTwo.isDisabled()).toBe(true); +}); +``` + +### Test Utils + +```tsx +const { getButton, isDisabled } = getTestUtils(); +``` + +| Util | Description | Returns | +| ------------ | ------------------------------------- | ------------------- | +| `getButton` | Returns the input node | `HTMLButtonElement` | +| `isDisabled` | Returns whether the input is disabled | `boolean` | diff --git a/packages/button/package.json b/packages/button/package.json index bc50d0dbe2..ca97771134 100644 --- a/packages/button/package.json +++ b/packages/button/package.json @@ -24,10 +24,11 @@ "dependencies": { "@leafygreen-ui/box": "^3.1.9", "@leafygreen-ui/emotion": "^4.0.8", - "@leafygreen-ui/lib": "^13.3.0", - "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/lib": "^13.4.0", + "@leafygreen-ui/palette": "^4.0.10", "@leafygreen-ui/ripple": "^1.1.13", "@leafygreen-ui/tokens": "^2.5.2", + "@lg-tools/test-harnesses": "^0.1.2", "polished": "^4.2.2" }, "devDependencies": { diff --git a/packages/button/src/Button/Button.spec.tsx b/packages/button/src/Button/Button.spec.tsx index 429478b9dc..1e13fe8b33 100644 --- a/packages/button/src/Button/Button.spec.tsx +++ b/packages/button/src/Button/Button.spec.tsx @@ -8,6 +8,7 @@ import { BoxProps } from '@leafygreen-ui/box'; import { Spinner } from '@leafygreen-ui/loading-indicator'; import { ButtonProps } from '../types'; +import { getTestUtils } from '../utils/getTestUtils'; import Button from '..'; const className = 'test-button-class'; @@ -16,8 +17,9 @@ const child = 'Button child'; function renderButton(props: BoxProps<'button', ButtonProps> = {}) { const utils = render(, render); + +function renderButton(props = {}) { + render(); +} + +function renderMultipleToggles() { + render( + <> + + + , + ); +} + +describe('packages/button/getTestUtils', () => { + describe('renders properly', () => { + test('throws error if LG Button is not found', () => { + render(); + + try { + const _utils = getTestUtils(); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toHaveProperty( + 'message', + expect.stringMatching( + /Unable to find an element by: \[data-lgid="lg-button"\]/, + ), + ); + } + }); + }); + + describe('single button', () => { + test('getButton', () => { + renderButton(); + const { getButton } = getTestUtils(); + + expect(getButton()).toBeInTheDocument(); + }); + + describe('isDisabled', () => { + test('to be false', () => { + renderButton(); + const { isDisabled } = getTestUtils(); + + expect(isDisabled()).toBe(false); + }); + + test('to be true', () => { + renderButton({ disabled: true }); + const { isDisabled } = getTestUtils(); + + expect(isDisabled()).toBe(true); + }); + }); + }); + + describe('multiple toggles', () => { + test('getButton', () => { + renderMultipleToggles(); + const utilsOne = getTestUtils('lg-Button-1'); + const utilsTwo = getTestUtils('lg-Button-2'); + + expect(utilsOne.getButton()).toBeInTheDocument(); + expect(utilsTwo.getButton()).toBeInTheDocument(); + }); + }); + + describe('async component', () => { + test('find LG Button after awaiting an async component', async () => { + const { openButton, findByTestId, asyncTestComponentId } = + renderButtonAsync(); + + userEvent.click(openButton); + + const asyncComponent = await findByTestId(asyncTestComponentId); + expect(asyncComponent).toBeInTheDocument(); + + // After awaiting asyncComponent, look for button + const { getButton } = getTestUtils(); + expect(getButton()).toBeInTheDocument(); + }); + + test('find LG Button after awaiting getTestUtils', async () => { + const { openButton } = renderButtonAsync(); + + userEvent.click(openButton); + + // awaiting getTestUtils + await waitFor(() => { + const { getButton } = getTestUtils(); + expect(getButton()).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/packages/button/src/utils/getTestUtils.tsx b/packages/button/src/utils/getTestUtils.tsx new file mode 100644 index 0000000000..f90e5f3689 --- /dev/null +++ b/packages/button/src/utils/getTestUtils.tsx @@ -0,0 +1,27 @@ +import { getByLgId } from '@lg-tools/test-harnesses'; + +import { LGIDS_BUTTON } from '../constants'; + +import { GetTestUtilsReturnType } from './getTestUtils.types'; + +export const getTestUtils = ( + lgId: string = LGIDS_BUTTON.root, +): GetTestUtilsReturnType => { + /** + * Queries the DOM for the element using the `data-lgid` data attribute. + * Will throw if no element is found. + */ + const element: T = getByLgId!(lgId); + + /** + * Returns the disabled attribute on the input. + */ + const isButtonDisabled = () => { + return element.getAttribute('aria-disabled') === 'true'; + }; + + return { + getButton: () => element, + isDisabled: () => isButtonDisabled(), + }; +}; diff --git a/packages/button/src/utils/getTestUtils.types.tsx b/packages/button/src/utils/getTestUtils.types.tsx new file mode 100644 index 0000000000..7865880570 --- /dev/null +++ b/packages/button/src/utils/getTestUtils.types.tsx @@ -0,0 +1,6 @@ +import { FormUtils } from '@lg-tools/test-harnesses'; + +export interface GetTestUtilsReturnType { + getButton: () => T; + isDisabled: FormUtils['isDisabled']; +} diff --git a/packages/button/src/utils/index.ts b/packages/button/src/utils/index.ts new file mode 100644 index 0000000000..edf2ec7e5a --- /dev/null +++ b/packages/button/src/utils/index.ts @@ -0,0 +1 @@ +export { getTestUtils } from './getTestUtils'; diff --git a/packages/checkbox/README.md b/packages/checkbox/README.md index 0160c3de81..3b80c1035d 100644 --- a/packages/checkbox/README.md +++ b/packages/checkbox/README.md @@ -84,3 +84,124 @@ import Checkbox from '@leafygreen-ui/checkbox'; | ... | native `input` attributes | Any other props will be spread on the root `input` element | | _Any other properties will be spread on the `input` element._ + +# Test Harnesses + +## getTestUtils() + +`getTestUtils()` is a util that allows consumers to reliably interact with `LG Checkbox` in a product test suite. If the `Checkbox` component cannot be found, an error will be thrown. + +### Usage + +```tsx +import Checkbox, { getTestUtils } from '@leafygreen-ui/checkbox'; + +const utils = getTestUtils(lgId?: string); // lgId refers to the custom `data-lgid` attribute passed to `Checkbox`. It defaults to 'lg-checkbox' if left empty. +``` + +#### Single `Checkbox` + +```tsx +import { render } from '@testing-library/react'; +import Checkbox, { getTestUtils } from '@leafygreen-ui/checkbox'; + +... + +test('checkbox', () => { + render(); + const { getInput, getInputValue } = getTestUtils(); + + expect(getInput()).toBeInTheDocument(); + expect(getInputValue()).toBe(true); +}); +``` + +#### Multiple `Checkbox` components + +When testing multiple `Checkbox` components it is recommended to add the custom `data-lgid` attribute to each `Checkbox`. + +```tsx +import { render } from '@testing-library/react'; +import Checkbox, { getTestUtils } from '@leafygreen-ui/checkbox'; + +... + +test('checkbox', () => { + render( + <> + + + , + ); + const utilsOne = getTestUtils('checkbox-1'); // data-lgid + const utilsTwo = getTestUtils('checkbox-2'); // data-lgid + // First Checkbox + expect(utilsOne.checkboxetInput()).toBeInTheDocument(); + expect(utilsOne.getInputValue()).toBe(false); + + // Second Checkbox + expect(utilsTwo.getInput()).toBeInTheDocument(); + expect(utilsTwo.getInputValue()).toBe(true); +}); +``` + +#### Checkbox with other LG form elements + +```tsx +import { render } from '@testing-library/react'; +import Toggle, { getTestUtils as getLGToggleTestUtils } from '@leafygreen-ui/toggle'; +import TextInput, { getTestUtils as getLGTextInputTestUtils } from '@leafygreen-ui/text-input'; +import Checkbox, { getTestUtils } from '@leafygreen-ui/checkbox'; + +... + +test('Form', () => { + render( +
+ + + + , + ); + + const toggleInputUtils = getLGToggleTestUtils(); + const textInputUtils = getLGTextInputTestUtils(); + const checkboxUtils = getTestUtils(); + + // LG Toggle + expect(toggleInputUtils.getInput()).toBeInTheDocument(); + expect(toggleInputUtils.getInputValue()).toBe('false'); + + // LG TextInput + expect(textInputUtils.getInput()).toBeInTheDocument(); + expect(textInputUtils.getInputValue()).toBe(''); + + // LG Checkbox + expect(checkboxUtils.getInput()).toBeInTheDocument(); + expect(checkboxUtils.getInputValue()).toBe(false); +}); +``` + +### Test Utils + +#### Elements + +```tsx +const { + getInput, + getLabel, + getDescription, + getInputValue, + isDisabled, + isIndeterminate, +} = getTestUtils(); +``` + +| Util | Description | Returns | +| ----------------- | ------------------------------------------ | ----------------------------- | +| `getInput` | Returns the input node | `HTMLButtonElement` | +| `getLabel` | Returns the label node | `HTMLButtonElement` \| `null` | +| `getDescription` | Returns the description node | `HTMLButtonElement` \| `null` | +| `getInputValue` | Returns the input value | `boolean` | +| `isDisabled` | Returns whether the input is disabled | `boolean` | +| `isIndeterminate` | Returns whether the input is indeterminate | `boolean` | diff --git a/packages/checkbox/package.json b/packages/checkbox/package.json index 09c785bb5c..c637437865 100644 --- a/packages/checkbox/package.json +++ b/packages/checkbox/package.json @@ -25,10 +25,11 @@ "@leafygreen-ui/a11y": "^1.4.13", "@leafygreen-ui/emotion": "^4.0.8", "@leafygreen-ui/hooks": "^8.1.3", - "@leafygreen-ui/lib": "^13.3.0", - "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/lib": "^13.4.0", + "@leafygreen-ui/palette": "^4.0.10", "@leafygreen-ui/tokens": "^2.5.2", - "@leafygreen-ui/typography": "^18.3.0", + "@leafygreen-ui/typography": "^18.4.0", + "@lg-tools/test-harnesses": "^0.1.2", "react-transition-group": "^4.4.5" }, "peerDependencies": { diff --git a/packages/checkbox/src/Checkbox/Checkbox.spec.tsx b/packages/checkbox/src/Checkbox/Checkbox.spec.tsx index ce576482a4..18647d5c1e 100644 --- a/packages/checkbox/src/Checkbox/Checkbox.spec.tsx +++ b/packages/checkbox/src/Checkbox/Checkbox.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { act, fireEvent, render } from '@testing-library/react'; import { axe } from 'jest-axe'; +import { getTestUtils } from '../utils/getTestUtils'; import Checkbox from '..'; const className = 'test-classname'; @@ -9,13 +10,14 @@ const onChange = jest.fn(); const onClick = jest.fn(); function renderCheckbox(props = {}) { - const utils = render( + const renderUtils = render( , ); - const wrapper = utils.container.firstElementChild; - const checkbox = utils.getByTestId('checkbox'); - const label = utils.container.querySelector('label'); - return { ...utils, wrapper, checkbox, label }; + const utils = getTestUtils(); + const wrapper = renderUtils.container.firstElementChild; + const checkbox = utils.getInput(); + const label = utils.getLabel(); + return { ...renderUtils, ...utils, wrapper, checkbox, label }; } describe('packages/checkbox', () => { @@ -34,26 +36,31 @@ describe('packages/checkbox', () => { }); }); + test('uses the aria-label prop when supplied', () => { + const { checkbox } = renderCheckbox({ 'aria-label': 'test string' }); + expect(checkbox.getAttribute('aria-label')).toBe('test string'); + }); + test(`passes \`className\` through to checkbox parent`, () => { const { wrapper } = renderCheckbox({ className }); expect(wrapper?.classList).toContain(className); }); test('renders as unchecked by default', () => { - const { checkbox } = renderCheckbox(); - expect((checkbox as HTMLInputElement).checked).toBe(false); + const { checkbox, getInputValue } = renderCheckbox(); + expect(getInputValue()).toBe(false); expect(checkbox.getAttribute('aria-checked')).toBe('false'); }); test('renders as checked when the prop is set', () => { - const { checkbox } = renderCheckbox({ checked: true }); - expect((checkbox as HTMLInputElement).checked).toBe(true); + const { checkbox, getInputValue } = renderCheckbox({ checked: true }); + expect(getInputValue()).toBe(true); expect(checkbox.getAttribute('aria-checked')).toBe('true'); }); test('renders with aria-disabled attribute but not disabled attribute when disabled prop is set', () => { - const { checkbox } = renderCheckbox({ disabled: true }); - expect(checkbox.getAttribute('aria-disabled')).toBeTruthy(); + const { checkbox, isDisabled } = renderCheckbox({ disabled: true }); + expect(isDisabled).toBeTruthy(); expect(checkbox.getAttribute('disabled')).toBeFalsy(); }); @@ -63,7 +70,7 @@ describe('packages/checkbox', () => { }); test('renders as indeterminate when prop is set and checkbox is true', () => { - const { checkbox, rerender } = renderCheckbox({ + const { checkbox, getInputValue, rerender } = renderCheckbox({ indeterminate: true, checked: true, }); @@ -77,6 +84,7 @@ describe('packages/checkbox', () => { />, ); expect(checkbox.getAttribute('aria-checked')).toBe('true'); + expect(getInputValue()).toBe(true); }); describe('when controlled', () => { @@ -93,9 +101,9 @@ describe('packages/checkbox', () => { }); test('checkbox does not become checked when clicked', () => { - const { checkbox } = renderCheckbox({ checked: false }); + const { checkbox, getInputValue } = renderCheckbox({ checked: false }); fireEvent.click(checkbox); - expect((checkbox as HTMLInputElement).checked).toBe(false); + expect(getInputValue()).toBe(false); }); }); @@ -116,9 +124,9 @@ describe('packages/checkbox', () => { }); test('checkbox becomes checked when clicked', () => { - const { checkbox } = renderCheckbox({}); + const { checkbox, getInputValue } = renderCheckbox({}); fireEvent.click(checkbox); - expect((checkbox as HTMLInputElement).checked).toBe(true); + expect(getInputValue()).toBe(true); }); }); }); diff --git a/packages/checkbox/src/Checkbox/Checkbox.tsx b/packages/checkbox/src/Checkbox/Checkbox.tsx index aa01fb8ea9..b86445690b 100644 --- a/packages/checkbox/src/Checkbox/Checkbox.tsx +++ b/packages/checkbox/src/Checkbox/Checkbox.tsx @@ -14,6 +14,7 @@ import { } from '@leafygreen-ui/typography'; import { Check } from '../Check'; +import { LGIDS_CHECKBOX } from '../constants'; import { checkWrapperClassName, @@ -37,11 +38,13 @@ import { CheckboxProps } from './Checkbox.types'; */ function Checkbox({ animate = true, + 'aria-label': ariaLabel = 'checkbox', baseFontSize: baseFontSizeProp, bold: boldProp, checked: checkedProp, className, darkMode: darkModeProp, + 'data-lgid': dataLgId = LGIDS_CHECKBOX.root, description, disabled = false, id: idProp, @@ -107,6 +110,7 @@ function Checkbox({ }, className, )} + data-lgid={dataLgId} style={style} >