diff --git a/cypress/component/Alerts.cy.tsx b/cypress/component/Alerts.cy.tsx new file mode 100644 index 0000000000..90eb378a83 --- /dev/null +++ b/cypress/component/Alerts.cy.tsx @@ -0,0 +1,51 @@ +/* + * 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 from 'react' +import { Alert } from '../../packages/ui' + +import '../support/component' +import 'cypress-real-events' + +describe('', () => { + it('should have shadow by default', async () => { + cy.mount( + + Success: Sample alert text. + + ) + cy.get('div[class$="-view-alert"]') + .should('have.css', 'box-shadow') + .and('not.equal', 'none') + }) + + it("shouldn't have shadow, when `hasShadow` is set to false", async () => { + cy.mount( + + Success: Sample alert text. + + ) + + cy.get('div[class$="-view-alert"]').should('have.css', 'box-shadow', 'none') + }) +}) diff --git a/package-lock.json b/package-lock.json index 87fabc3ab1..fdf60031fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41137,15 +41137,73 @@ "prop-types": "^15.8.1" }, "devDependencies": { + "@instructure/ui-axe-check": "10.2.0", "@instructure/ui-babel-preset": "10.2.0", "@instructure/ui-color-utils": "10.2.0", - "@instructure/ui-test-utils": "10.2.0" + "@instructure/ui-scripts": "10.2.0", + "@instructure/ui-test-utils": "10.2.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.5.2", + "vitest": "^2.0.2" }, "peerDependencies": { "react": ">=16.8 <=18", "react-dom": ">=16.8 <=18" } }, + "packages/ui-alerts/node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "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-alerts/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-alerts/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-avatar": { "name": "@instructure/ui-avatar", "version": "10.2.0", diff --git a/packages/ui-alerts/package.json b/packages/ui-alerts/package.json index a17b5fe1bb..e57393a1f1 100644 --- a/packages/ui-alerts/package.json +++ b/packages/ui-alerts/package.json @@ -23,9 +23,15 @@ }, "license": "MIT", "devDependencies": { + "@instructure/ui-axe-check": "10.2.0", "@instructure/ui-babel-preset": "10.2.0", "@instructure/ui-color-utils": "10.2.0", - "@instructure/ui-test-utils": "10.2.0" + "@instructure/ui-scripts": "10.2.0", + "@instructure/ui-test-utils": "10.2.0", + "@testing-library/user-event": "^14.5.2", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^15.0.7", + "vitest": "^2.0.2" }, "dependencies": { "@babel/runtime": "^7.24.5", diff --git a/packages/ui-alerts/src/Alert/__new-tests__/Alert.test.tsx b/packages/ui-alerts/src/Alert/__new-tests__/Alert.test.tsx new file mode 100644 index 0000000000..32a4339526 --- /dev/null +++ b/packages/ui-alerts/src/Alert/__new-tests__/Alert.test.tsx @@ -0,0 +1,275 @@ +/* + * 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 from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import { runAxeCheck } from '@instructure/ui-axe-check' +import '@testing-library/jest-dom' +import userEvent from '@testing-library/user-event' + +import { Alert } from '../index' +import type { AlertProps } from '../props' +// eslint-disable-next-line no-restricted-imports +import { generateA11yTests } from '@instructure/ui-scripts/lib/test/generateA11yTests' +import AlertExamples from '../__examples__/Alert.examples' + +describe('', () => { + let srdiv: HTMLDivElement | null + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(async () => { + // Mocking console to prevent test output pollution and expect + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + srdiv = document.createElement('div') + srdiv.id = '_alertLiveRegion' + srdiv.setAttribute('role', 'alert') + srdiv.setAttribute('aria-live', 'assertive') + srdiv.setAttribute('aria-relevant', 'additions text') + srdiv.setAttribute('aria-atomic', 'false') + document.body.appendChild(srdiv) + }) + + afterEach(async () => { + srdiv?.parentNode?.removeChild(srdiv) + srdiv = null + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it('should render', async () => { + render(Success: Sample alert text.) + const text = screen.getByText('Success: Sample alert text.') + expect(text).toBeInTheDocument() + }) + + describe('with generated examples', () => { + const generatedComponents = generateA11yTests(Alert, AlertExamples) + + for (const component of generatedComponents) { + it(component.description, async () => { + const { container } = render(component.content) + const axeCheck = await runAxeCheck(container) + expect(axeCheck).toBe(true) + }) + } + }) + + it('should not render the Close button when `renderCloseButtonLabel` is not provided', async () => { + render(Success: Sample alert text.) + const closeButton = screen.queryByRole('button') + + expect(closeButton).not.toBeInTheDocument() + }) + + it('should call `onDismiss` when the close button is clicked with renderCloseButtonLabel', async () => { + const onDismiss = vi.fn() + render( + Close} + onDismiss={onDismiss} + > + Success: Sample alert text. + + ) + const closeButton = screen.getByRole('button') + + userEvent.click(closeButton) + + await waitFor(() => { + expect(onDismiss).toHaveBeenCalled() + }) + }) + + const iconComponentsVariants: Record< + NonNullable, + string + > = { + error: 'IconNo', + info: 'IconInfoBorderless', + success: 'IconCheckMark', + warning: 'IconWarningBorderless' + } + + ;( + Object.entries(iconComponentsVariants) as [ + NonNullable, + string + ][] + ).forEach(([variant, iconComponent]) => { + it(`"${variant}" variant should have icon "${iconComponent}".`, async () => { + const { container } = render( + + Success: Sample alert text. + + ) + const icon = container.querySelector('svg[class$="-svgIcon"]') + + expect(icon).toHaveAttribute('name', iconComponent) + }) + }) + + it('should meet a11y standards', async () => { + const { container } = render( + + Success: Sample alert text. + + ) + const axeCheck = await runAxeCheck(container) + expect(axeCheck).toBe(true) + }) + + it('should add alert text to aria live region, when present', async () => { + const liveRegion = document.getElementById('_alertLiveRegion')! + render( + liveRegion} + liveRegionPoliteness="polite" + > + Success: Sample alert text. + + ) + + expect(liveRegion).toHaveTextContent('Success: Sample alert text.') + expect(liveRegion).toHaveAttribute('aria-live', 'polite') + }) + + describe('with `screenReaderOnly', () => { + it('should not render anything when using `liveRegion`', async () => { + const liveRegion = document.getElementById('_alertLiveRegion')! + const { container } = render( + liveRegion} + screenReaderOnly={true} + > + Success: Sample alert text. asdsfds + + ) + + expect(container.children.length).toBe(0) + expect(liveRegion.children.length).toBe(1) + }) + + it('should warn if `liveRegion` is not defined', async () => { + const consoleWarningSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) + const warning = + "Warning: [Alert] The 'screenReaderOnly' prop must be used in conjunction with 'liveRegion'." + render( + + Success: Sample alert text. + + ) + + await waitFor(() => { + expect(consoleWarningSpy.mock.calls[0][0]).toEqual( + expect.stringContaining(warning) + ) + }) + }) + + it('should set aria-atomic to the aria live region when isLiveRegionAtomic is present', async () => { + const liveRegion = document.getElementById('_alertLiveRegion')! + render( + liveRegion} + liveRegionPoliteness="polite" + isLiveRegionAtomic + > + Success: Sample alert text. + + ) + + expect(liveRegion).toHaveTextContent('Success: Sample alert text.') + expect(liveRegion).toHaveAttribute('aria-atomic', 'true') + }) + + it('should close when told to, with transition', async () => { + const liveRegion = document.getElementById('_alertLiveRegion')! + const { rerender } = render( + liveRegion}> + Success: Sample alert text. + + ) + + expect(liveRegion.children.length).toBe(1) + + //set open to false + rerender( + liveRegion}> + Success: Sample alert text. + + ) + + await waitFor(() => { + expect(liveRegion.children.length).toBe(0) + }) + }) + + it('should close when told to, without transition', async () => { + const liveRegion = document.getElementById('_alertLiveRegion')! + const { rerender, container } = render( + liveRegion} + > + Success: Sample alert text. + + ) + + expect(liveRegion.children.length).toBe(1) + + //set open to false + rerender( + liveRegion} + > + Success: Sample alert text. + + ) + + await waitFor(() => { + expect(container).not.toHaveTextContent('Success: Sample alert text.') + expect(liveRegion.children.length).toBe(0) + }) + }) + }) +}) diff --git a/packages/ui-alerts/src/Alert/__tests__/Alert.test.tsx b/packages/ui-alerts/src/Alert/__tests__/Alert.test.tsx deleted file mode 100644 index 5dd57353b2..0000000000 --- a/packages/ui-alerts/src/Alert/__tests__/Alert.test.tsx +++ /dev/null @@ -1,278 +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 React from 'react' - -import { - expect, - mount, - stub, - wait, - within, - generateA11yTests -} from '@instructure/ui-test-utils' - -import { Alert } from '../index' -import AlertExamples from '../__examples__/Alert.examples' -import type { AlertProps } from '../props' - -describe('', async () => { - let srdiv: HTMLDivElement | null - - beforeEach(async () => { - stub(console, 'warn') // suppress deprecation warnings - srdiv = document.createElement('div') - srdiv.id = '_alertLiveRegion' - srdiv.setAttribute('role', 'alert') - srdiv.setAttribute('aria-live', 'assertive') - srdiv.setAttribute('aria-relevant', 'additions text') - srdiv.setAttribute('aria-atomic', 'false') - document.body.appendChild(srdiv) - }) - - afterEach(async () => { - srdiv?.parentNode?.removeChild(srdiv) - srdiv = null - }) - - it('should render', async () => { - const subject = await mount( - Success: Sample alert text. - ) - - expect(subject.getDOMNode()).to.exist() - }) - - describe('with generated examples', async () => { - generateA11yTests(Alert, AlertExamples) - }) - - it('should not render the Close button when `renderCloseButtonLabel` is not provided', async () => { - const subject = await mount( - Success: Sample alert text. - ) - - const alert = within(subject.getDOMNode()) - const closeButton = await alert.find(':focusable', { - expectEmpty: true - }) - - expect(closeButton).to.not.exist() - }) - - // TODO fix test when new testing library is introduced - // it('should call `onDismiss` when the close button is clicked with renderCloseButtonLabel', async () => { - // const onDismiss = stub() - // const subject = await mount( - // Close} - // onDismiss={onDismiss} - // > - // Success: Sample alert text. - // - // ) - // - // const alert = within(subject.getDOMNode()) - // const closeButton = await alert.find(':focusable') - // - // await closeButton.click() - // await wait(() => { - // expect(onDismiss).to.have.been.called() - // }) - // }) - - const iconComponentsVariants: Record< - NonNullable, - string - > = { - error: 'IconNo', - info: 'IconInfoBorderless', - success: 'IconCheckMark', - warning: 'IconWarningBorderless' - } - - ;( - Object.entries(iconComponentsVariants) as [ - NonNullable, - string - ][] - ).forEach(([variant, iconComponent]) => { - it(`"${variant}" variant should have icon "${iconComponent}".`, async () => { - const subject = await mount( - - Success: Sample alert text. - - ) - - const alert = within(subject.getDOMNode()) - const icon = await alert.find(`[name=${iconComponent}]`) - expect(icon).to.exist() - }) - }) - - it('should meet a11y standards', async () => { - const subject = await mount( - - Success: Sample alert text. - - ) - - const alert = within(subject.getDOMNode()) - expect(await alert.accessible()).to.be.true() - }) - - it('should add alert text to aria live region, when present', async () => { - const liver = document.getElementById('_alertLiveRegion')! - await mount( - liver} - liveRegionPoliteness="polite" - > - Success: Sample alert text. - - ) - - expect(liver.innerText).to.include('Success: Sample alert text.') - - expect(liver.getAttribute('aria-live')).to.equal('polite') - }) - - describe('with `screenReaderOnly', async () => { - it('should not render anything when using `liveRegion`', async () => { - const liver = document.getElementById('_alertLiveRegion')! - await mount( - liver} - screenReaderOnly={true} - > - Success: Sample alert text. - - ) - - const root = document.querySelector('[data-ui-test-utils]')! - - expect(root.children.length).to.equal(0) - expect(liver.children.length).to.equal(1) - }) - - it('should warn if `liveRegion` is not defined', async () => { - const consoleError = stub(console, 'error') - const warning = - "Warning: [Alert] The 'screenReaderOnly' prop must be used in conjunction with 'liveRegion'." - await mount( - - Success: Sample alert text. - - ) - expect(consoleError).to.be.calledWith(warning) - }) - }) - - it('should set aria-atomic to the aria live region when isLiveRegionAtomic is present', async () => { - const liver = document.getElementById('_alertLiveRegion')! - await mount( - liver} - liveRegionPoliteness="polite" - isLiveRegionAtomic - > - Success: Sample alert text. - - ) - - expect(liver.innerText).to.include('Success: Sample alert text.') - expect(liver.getAttribute('aria-atomic')).to.equal('true') - }) - - it('should close when told to, with transition', async () => { - const liver = document.getElementById('_alertLiveRegion')! - const subject = await mount( - liver}> - Success: Sample alert text. - - ) - - expect(liver.children.length).to.equal(1) - - await subject.setProps({ - open: false - }) - - await wait(() => { - expect(liver.children.length).to.equal(0) - }) - }) - - it('should close when told to, without transition', async () => { - const liver = document.getElementById('_alertLiveRegion')! - const subject = await mount( - liver}> - Success: Sample alert text. - - ) - - expect(liver.children.length).to.equal(1) - - await subject.setProps({ - open: false - }) - - expect(subject.getDOMNode()).to.not.exist() - expect(liver.children.length).to.equal(0) - }) - - it('should have shadow by default', async () => { - const subject = await mount( - - Success: Sample alert text. - - ) - - const alert = within(subject.getDOMNode()).getDOMNode() - const computedStyle = getComputedStyle(alert) - - expect(computedStyle.boxShadow).to.equal( - 'rgba(0, 0, 0, 0.1) 0px 3px 6px 0px, rgba(0, 0, 0, 0.16) 0px 3px 6px 0px' - ) - }) - - it("shouldn't have shadow, when `hasShadow` is set to false", async () => { - const subject = await mount( - - Success: Sample alert text. - - ) - - const alert = within(subject.getDOMNode()).getDOMNode() - const computedStyle = getComputedStyle(alert) - - expect(computedStyle.boxShadow).to.equal('none') - }) -}) diff --git a/packages/ui-alerts/tsconfig.build.json b/packages/ui-alerts/tsconfig.build.json index 407eadaa05..bacfb19666 100644 --- a/packages/ui-alerts/tsconfig.build.json +++ b/packages/ui-alerts/tsconfig.build.json @@ -14,10 +14,12 @@ { "path": "../emotion/tsconfig.build.json" }, { "path": "../shared-types/tsconfig.build.json" }, { "path": "../ui-a11y-content/tsconfig.build.json" }, + { "path": "../ui-axe-check/tsconfig.build.json" }, { "path": "../ui-buttons/tsconfig.build.json" }, { "path": "../ui-icons/tsconfig.build.json" }, { "path": "../ui-motion/tsconfig.build.json" }, { "path": "../ui-react-utils/tsconfig.build.json" }, + { "path": "../ui-scripts/tsconfig.build.json" }, { "path": "../ui-themes/tsconfig.build.json" }, { "path": "../ui-view/tsconfig.build.json" } ]