From a0670f362db2bdc11a8f1aba3893ba3c4400ef06 Mon Sep 17 00:00:00 2001 From: Tamas Kovacs Date: Wed, 10 Jul 2024 15:46:51 +0200 Subject: [PATCH] test(ui-link): migrate old Link tests Closes: INSTUI-4119 --- package-lock.json | 60 ++- packages/ui-link/package.json | 8 +- packages/ui-link/src/Link/LinkLocator.ts | 35 -- .../src/Link/__new-tests__/Link.test.tsx | 415 ++++++++++++++++++ .../ui-link/src/Link/__tests__/Link.test.tsx | 395 ----------------- packages/ui-link/src/Link/locator.ts | 28 -- packages/ui-link/tsconfig.build.json | 3 +- 7 files changed, 478 insertions(+), 466 deletions(-) delete mode 100644 packages/ui-link/src/Link/LinkLocator.ts create mode 100644 packages/ui-link/src/Link/__new-tests__/Link.test.tsx delete mode 100644 packages/ui-link/src/Link/__tests__/Link.test.tsx delete mode 100644 packages/ui-link/src/Link/locator.ts diff --git a/package-lock.json b/package-lock.json index e0c02521b1..23c4f8485a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41438,16 +41438,70 @@ "prop-types": "^15.8.1" }, "devDependencies": { + "@instructure/ui-axe-check": "9.2.0", "@instructure/ui-babel-preset": "9.2.0", - "@instructure/ui-test-locator": "9.2.0", - "@instructure/ui-test-queries": "9.2.0", "@instructure/ui-test-utils": "9.2.0", - "@instructure/ui-themes": "9.2.0" + "@instructure/ui-themes": "9.2.0", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.5.2" }, "peerDependencies": { "react": ">=16.8 <=18" } }, + "packages/ui-link/node_modules/@testing-library/dom": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.1.tgz", + "integrity": "sha512-q/WL+vlXMpC0uXDyfsMtc1rmotzLV8Y0gq6q1gfrrDjQeHoeLrqHbxdPvPNAh1i+xuJl7+BezywcXArz7vLqKQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "packages/ui-link/node_modules/@testing-library/react": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^10.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "packages/ui-link/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "packages/ui-list": { "name": "@instructure/ui-list", "version": "9.2.0", diff --git a/packages/ui-link/package.json b/packages/ui-link/package.json index bf5537e9eb..5e1c781760 100644 --- a/packages/ui-link/package.json +++ b/packages/ui-link/package.json @@ -38,11 +38,13 @@ "prop-types": "^15.8.1" }, "devDependencies": { + "@instructure/ui-axe-check": "9.2.0", "@instructure/ui-babel-preset": "9.2.0", - "@instructure/ui-test-locator": "9.2.0", - "@instructure/ui-test-queries": "9.2.0", "@instructure/ui-test-utils": "9.2.0", - "@instructure/ui-themes": "9.2.0" + "@instructure/ui-themes": "9.2.0", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.5.2" }, "peerDependencies": { "react": ">=16.8 <=18" diff --git a/packages/ui-link/src/Link/LinkLocator.ts b/packages/ui-link/src/Link/LinkLocator.ts deleted file mode 100644 index 09962d5c21..0000000000 --- a/packages/ui-link/src/Link/LinkLocator.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import { locator } from '@instructure/ui-test-locator' -import { find } from '@instructure/ui-test-queries' - -import { Link } from './index' - -// @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message -export const LinkLocator = locator(Link.selector, { - click: async (element: any, ...args: any[]) => { - return (await find(element, 'a,button,[role="button"]')).click(...args) - } -}) diff --git a/packages/ui-link/src/Link/__new-tests__/Link.test.tsx b/packages/ui-link/src/Link/__new-tests__/Link.test.tsx new file mode 100644 index 0000000000..e8ca87b28c --- /dev/null +++ b/packages/ui-link/src/Link/__new-tests__/Link.test.tsx @@ -0,0 +1,415 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { PropsWithChildren } from 'react' +import { userEvent } from '@testing-library/user-event' +import { render, screen, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { runAxeCheck } from '@instructure/ui-axe-check' +import { Link } from '../index' + +// is needed for React 17 compatibility +class TruncateText extends React.Component> { + render() { + return {this.props.children} + } +} + +describe('', () => { + let consoleErrorMock: jest.SpyInstance + + beforeEach(() => { + // Mocking console to prevent test output pollution + consoleErrorMock = jest.spyOn(console, 'error').mockImplementation() + }) + + afterEach(() => { + consoleErrorMock.mockRestore() + }) + + it('should render the children as text content', async () => { + render(Hello World) + const link = screen.getByRole('link') + + expect(link).toHaveTextContent('Hello World') + }) + + it('should render a button', async () => { + const onClick = jest.fn() + render(Hello World) + const button = screen.getByRole('button') + + expect(button).toHaveAttribute('type', 'button') + }) + + it('should meet a11y standards', async () => { + const { container } = render( + Hello World + ) + const axeCheck = await runAxeCheck(container) + + expect(axeCheck).toBe(true) + }) + + it('should focus with the focus helper', async () => { + let linkRef: any + render( + { + linkRef = el + }} + > + Hello World + + ) + const linkElement = screen.getByRole('link') + + linkRef.focus() + + expect(linkElement).toHaveFocus() + }) + + it('should display block when TruncateText is a child', async () => { + render( + + Hello World + + ) + const link = screen.getByRole('link') + + expect(link).toHaveStyle('display: block') + }) + + it('should display inline-flex when TruncateText is a child and there is an icon', async () => { + const customIcon = ( + + Custom icon + + + ) + render( + + Hello World + + ) + const link = screen.getByRole('link') + + expect(link).toHaveStyle('display: inline-flex') + }) + + it('should call the onClick prop when clicked', async () => { + const onClick = jest.fn() + render(Hello World) + const link = screen.getByRole('button') + + userEvent.click(link) + + await waitFor(() => { + expect(onClick).toHaveBeenCalledTimes(1) + }) + }) + + it('should call the onMouseEnter prop when mouseEntered', async () => { + const onMouseEnter = jest.fn() + const { container } = render( + Hello World + ) + const link = container.querySelector('span[class$="-link"]')! + + userEvent.hover(link) + + await waitFor(() => { + expect(onMouseEnter).toHaveBeenCalledTimes(1) + }) + }) + + it('should pass down an icon via the icon property', async () => { + const customIcon = ( + + Custom icon + + + ) + render( + + Hello World + + ) + const title = screen.getByText('Custom icon') + const icon = screen.getByTestId('svg') + + expect(title).toBeInTheDocument() + expect(icon).toBeInTheDocument() + }) + + describe('when interaction is disabled', () => { + it('should apply aria-disabled when interaction is disabled', async () => { + render( + + Hello World + + ) + const link = screen.getByRole('link') + + expect(link).toHaveAttribute('aria-disabled') + }) + }) + + it('should apply aria-disabled when `disabled` is set', async () => { + render( + + Hello World + + ) + const link = screen.getByRole('link') + + expect(link).toHaveAttribute('aria-disabled') + }) + + it('should not be clickable when interaction is disabled', async () => { + const onClick = jest.fn() + render( + + Hello World + + ) + const button = screen.getByRole('button') + + expect(button).toHaveAttribute('aria-disabled', 'true') + }) + + it('should not be clickable when `disabled` is set', async () => { + const onClick = jest.fn() + render( + + Hello World + + ) + const button = screen.getByRole('button') + + expect(button).toHaveAttribute('aria-disabled', 'true') + }) +}) + +describe('with `as` prop', () => { + describe('with `onClick`', () => { + it('should render designated tag', async () => { + const onClick = jest.fn() + render( + + Hello World + + ) + const link = screen.getByText('Hello World') + + expect(link.tagName).toBe('A') + }) + + it('should set role="button"', async () => { + const onClick = jest.fn() + render( + + Hello World + + ) + const spanLink = screen.getByRole('button') + + expect(spanLink).toHaveAttribute('role', 'button') + }) + }) + + describe('should not set type="button", unless it is actually a button', () => { + it('should not set type="button" on other things like s', async () => { + const onClick = jest.fn() + render( + + Hello World + + ) + const link = screen.getByText('Hello World') + + expect(link).not.toHaveAttribute('type', 'button') + }) + + it('should set type="button" on