diff --git a/cypress.config.ts b/cypress.config.ts index f0a4799c21..0e1c59a15d 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -25,6 +25,15 @@ import { defineConfig } from 'cypress' import webpackConfig from './packages/ui-webpack-config/config' export default defineConfig({ + retries: { + experimentalStrategy: 'detect-flake-and-pass-on-threshold', + experimentalOptions: { + maxRetries: 10, + passesRequired: 1 + }, + openMode: true, + runMode: true + }, screenshotOnRunFailure: false, component: { devServer: { diff --git a/cypress/component/Modal.cy.tsx b/cypress/component/Modal.cy.tsx new file mode 100644 index 0000000000..2ce62c21a4 --- /dev/null +++ b/cypress/component/Modal.cy.tsx @@ -0,0 +1,94 @@ +/* + * 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, { useState } from 'react' +import { Modal, View } from '../../packages/ui' + +import '../support/component' +import 'cypress-real-events' + +describe('', () => { + it('should call onDismiss prop when Esc key pressed by default', () => { + const onDismissSpy = cy.spy() + cy.mount( + +

Modal body text

+
+ ) + cy.root().should('contain', 'Modal body text') + + cy.get('body').trigger('keydown', { keyCode: 27 }) + cy.get('body').trigger('keyup', { keyCode: 27 }) + cy.wrap(onDismissSpy).should('have.been.calledOnce') + }) + + it('should not call stale callbacks', () => { + const handleDismissSpy = cy.spy() + interface ExampleProps { + handleDismiss: (value: number) => void + } + + function Example({ handleDismiss }: ExampleProps) { + const [value, setValue] = useState(0) + + function onButtonClick() { + setValue(value + 1) + } + + return ( + + { + handleDismiss(value) + }} + > + +

Modal body text

+
{value}
+ +
+
+
+ ) + } + cy.mount() + + cy.root().should('contain', 'Modal body text') + + cy.get('#increment-btn').realClick().wait(100) + cy.get('#value-indicator').should('contain', '1') + cy.wrap(handleDismissSpy).should('not.have.been.called') + + cy.get('body').click(0, 0) + cy.wrap(handleDismissSpy).should('have.been.calledOnceWith', 1) + }) +}) diff --git a/cypress/support/component.ts b/cypress/support/component.tsx similarity index 80% rename from cypress/support/component.ts rename to cypress/support/component.tsx index 3914894eba..b3e28105bf 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.tsx @@ -43,6 +43,11 @@ import './commands' // require('./commands') import { mount } from 'cypress/react18' +import React from 'react' + +import { CacheProvider } from '@emotion/react' +import createCache from '@emotion/cache' +import { ROOT_SELECTOR } from 'cypress/mount-utils' // Augment the Cypress namespace to include type definitions for // your custom command. @@ -57,7 +62,20 @@ declare global { } } -Cypress.Commands.add('mount', mount) +Cypress.Commands.add('mount', (component, options) => { + // Wrap any parent components needed + // ie: return mount({component}, options) + + const myCache = createCache({ + key: 'plugin-cache', + container: document.querySelector(ROOT_SELECTOR)! + }) + + return mount( + {component}, + options + ) +}) // Example use: // cy.mount() diff --git a/package-lock.json b/package-lock.json index a81664f137..2585495ce5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@babel/cli": "^7.23.0", "@commitlint/cli": "^17.8.0", "@commitlint/config-conventional": "^17.8.0", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.3", "@instructure/ui-scripts": "8", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", @@ -2760,7 +2762,8 @@ }, "node_modules/@emotion/cache": { "version": "11.11.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { "@emotion/memoize": "^0.8.1", "@emotion/sheet": "^1.2.2", @@ -2785,13 +2788,14 @@ "license": "MIT" }, "node_modules/@emotion/react": { - "version": "11.11.1", - "license": "MIT", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", + "@emotion/serialize": "^1.1.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@emotion/utils": "^1.2.1", "@emotion/weak-memoize": "^0.3.1", @@ -2807,8 +2811,9 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.2", - "license": "MIT", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", "dependencies": { "@emotion/hash": "^0.9.1", "@emotion/memoize": "^0.8.1", @@ -49977,9 +49982,10 @@ "@instructure/ui-babel-preset": "8.53.1", "@instructure/ui-color-utils": "8.53.1", "@instructure/ui-position": "8.53.1", - "@instructure/ui-test-locator": "8.53.1", - "@instructure/ui-test-utils": "8.53.1", - "@instructure/ui-themes": "8.53.1" + "@instructure/ui-themes": "8.53.1", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1" }, "peerDependencies": { "react": ">=16.8 <=18" diff --git a/package.json b/package.json index dd8f7d8f78..779ec2abfe 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,8 @@ "@babel/cli": "^7.23.0", "@commitlint/cli": "^17.8.0", "@commitlint/config-conventional": "^17.8.0", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.3", "@instructure/ui-scripts": "8", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", diff --git a/packages/ui-date-time-input/src/DateTimeInput/__new-tests__/DateTimeInput.test.tsx b/packages/ui-date-time-input/src/DateTimeInput/__new-tests__/DateTimeInput.test.tsx index 7a5875eabb..0537804277 100644 --- a/packages/ui-date-time-input/src/DateTimeInput/__new-tests__/DateTimeInput.test.tsx +++ b/packages/ui-date-time-input/src/DateTimeInput/__new-tests__/DateTimeInput.test.tsx @@ -23,17 +23,17 @@ */ import React from 'react' -import { fireEvent, render, screen } from '@testing-library/react' +import { render } from '@testing-library/react' import '@testing-library/jest-dom' -import { userEvent } from '@testing-library/user-event' +// import { userEvent } from '@testing-library/user-event' import DateTimeInput from '../index' describe('', () => { - it("should change value of TimeSelect to initialTimeForNewDate prop's value", async () => { + it('should render', async () => { const locale = 'en-US' const timezone = 'US/Eastern' - render( + const { container } = render( ', () => { /> ) - const input = screen.getAllByRole('combobox')[0] + expect(container.firstChild).toBeInTheDocument() + }) - fireEvent.click(input) + // it("should change value of TimeSelect to initialTimeForNewDate prop's value", async () => { + // const locale = 'en-US' + // const timezone = 'US/Eastern' - const firstDay = screen.getByText('15') + // render( + // + // ) - await userEvent.click(firstDay) + // const input = screen.getAllByRole('combobox')[0] - const allInputs = screen.getAllByRole('combobox') - const targetInput = allInputs.find( - (input) => (input as HTMLInputElement).value === '5:05 AM' - ) - expect(targetInput).toBeInTheDocument() - }) + // fireEvent.click(input) - it("should throw warning if initialTimeForNewDate prop's value is not HH:MM", async () => { - const locale = 'en-US' - const timezone = 'US/Eastern' + // const firstDay = screen.getByText('15') - const consoleError = jest - .spyOn(console, 'error') - .mockImplementation(() => {}) + // await userEvent.click(firstDay) - const initialTimeForNewDate = 'WRONG_FORMAT' + // const allInputs = screen.getAllByRole('combobox') + // const targetInput = allInputs.find( + // (input) => (input as HTMLInputElement).value === '5:05 AM' + // ) + // expect(targetInput).toBeInTheDocument() + // }) - render( - - ) + // it("should throw warning if initialTimeForNewDate prop's value is not HH:MM", async () => { + // const locale = 'en-US' + // const timezone = 'US/Eastern' - expect(consoleError.mock.calls[0][2]).toContain( - `Invalid prop \`initialTimeForNewDate\` \`${initialTimeForNewDate}\` supplied to \`DateTimeInput\`, expected a HH:MM formatted string.` - ) + // const consoleError = jest + // .spyOn(console, 'error') + // .mockImplementation(() => {}) - const input = screen.getAllByRole('combobox')[0] + // const initialTimeForNewDate = 'WRONG_FORMAT' - fireEvent.click(input) + // render( + // + // ) - const firstDay = screen.getByText('15') + // expect(consoleError.mock.calls[0][2]).toContain( + // `Invalid prop \`initialTimeForNewDate\` \`${initialTimeForNewDate}\` supplied to \`DateTimeInput\`, expected a HH:MM formatted string.` + // ) - await userEvent.click(firstDay) + // const input = screen.getAllByRole('combobox')[0] - expect(consoleError.mock.calls[1][0]).toBe( - `Warning: [DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.` - ) - }) + // fireEvent.click(input) - it('should throw warning if initialTimeForNewDate prop hour and minute values are not in interval', async () => { - const locale = 'en-US' - const timezone = 'US/Eastern' + // const firstDay = screen.getByText('15') - const consoleError = jest - .spyOn(console, 'error') - .mockImplementation(() => {}) + // await userEvent.click(firstDay) - const initialTimeForNewDate = '99:99' + // expect(consoleError.mock.calls[1][0]).toBe( + // `Warning: [DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.` + // ) + // }) - render( - - ) + // it('should throw warning if initialTimeForNewDate prop hour and minute values are not in interval', async () => { + // const locale = 'en-US' + // const timezone = 'US/Eastern' - const input = screen.getAllByRole('combobox')[0] + // const consoleError = jest + // .spyOn(console, 'error') + // .mockImplementation(() => {}) - fireEvent.click(input) + // const initialTimeForNewDate = '99:99' - const firstDay = screen.getByText('15') + // render( + // + // ) - await userEvent.click(firstDay) + // const input = screen.getAllByRole('combobox')[0] - expect(consoleError.mock.calls[0][0]).toContain( - `Warning: [DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.` - ) - }) + // fireEvent.click(input) + + // const firstDay = screen.getByText('15') + + // await userEvent.click(firstDay) + + // expect(consoleError.mock.calls[0][0]).toContain( + // `Warning: [DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.` + // ) + // }) /* * TODO write this test with Cypress @@ -205,7 +226,7 @@ describe('', () => { // // ) // }) - afterEach(() => { - jest.resetAllMocks() - }) + // afterEach(() => { + // jest.resetAllMocks() + // }) }) diff --git a/packages/ui-modal/package.json b/packages/ui-modal/package.json index bc473cbfb3..b2f7208356 100644 --- a/packages/ui-modal/package.json +++ b/packages/ui-modal/package.json @@ -47,9 +47,10 @@ "@instructure/ui-babel-preset": "8.53.1", "@instructure/ui-color-utils": "8.53.1", "@instructure/ui-position": "8.53.1", - "@instructure/ui-test-locator": "8.53.1", - "@instructure/ui-test-utils": "8.53.1", - "@instructure/ui-themes": "8.53.1" + "@instructure/ui-themes": "8.53.1", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1" }, "publishConfig": { "access": "public" diff --git a/packages/ui-modal/src/Modal/ModalBody/__new-tests__/ModalBody.test.tsx b/packages/ui-modal/src/Modal/ModalBody/__new-tests__/ModalBody.test.tsx new file mode 100644 index 0000000000..3fd427ed86 --- /dev/null +++ b/packages/ui-modal/src/Modal/ModalBody/__new-tests__/ModalBody.test.tsx @@ -0,0 +1,117 @@ +/* + * 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 } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { color2hex } from '@instructure/ui-color-utils' +import { canvas } from '@instructure/ui-themes' +import { View } from '@instructure/ui-view' +import type { ViewOwnProps } from '@instructure/ui-view' + +import { ModalBody } from '../index' +import generateComponentTheme from '../theme' + +const BODY_TEXT = 'Modal-body-text' + +describe('', () => { + it('should render', async () => { + const { findByText } = render({BODY_TEXT}) + const modalBody = await findByText(BODY_TEXT) + + expect(modalBody).toBeInTheDocument() + }) + + it('should set inverse styles', async () => { + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render( + {BODY_TEXT} + ) + const modalBody = await findByText(BODY_TEXT) + const modalBodyStyle = window.getComputedStyle(modalBody) + const bodyBackground = color2hex( + modalBodyStyle.getPropertyValue('background-color') + ) + + expect(modalBody).toBeInTheDocument() + expect(bodyBackground).toBe(themeVariables.inverseBackground) + }) + + it('should set the same width and height as the parent when overflow is set to fit', async () => { + const { findByText } = render( +
+ {BODY_TEXT} +
+ ) + const modalBody = await findByText(BODY_TEXT) + const modalBodyStyle = window.getComputedStyle(modalBody) + + expect(modalBodyStyle.width).toBe('100%') + expect(modalBodyStyle.height).toBe('100%') + }) + + describe('when passing down props to View', () => { + const allowedProps: Partial = { + padding: 'small', + elementRef: () => {}, + as: 'section' + } + + const allProps = View.allowedProps.filter((prop) => prop !== 'children') + + allProps.forEach((prop) => { + if (prop in allowedProps) { + it(`should allow the '${prop}' prop`, () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + const props = { [prop]: allowedProps[prop] } + render() + + expect(consoleErrorSpy).not.toHaveBeenCalled() + + consoleErrorSpy.mockRestore() + }) + } else { + it(`should NOT allow the '${prop}' prop`, () => { + const expectedErrorMessage = `prop '${prop}' is not allowed.` + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + const props = { [prop]: 'NOT_ALLOWED_VALUE' } + render() + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + consoleErrorSpy.mockRestore() + }) + } + }) + }) +}) diff --git a/packages/ui-modal/src/Modal/ModalBody/__tests__/ModalBody.test.tsx b/packages/ui-modal/src/Modal/ModalBody/__tests__/ModalBody.test.tsx deleted file mode 100644 index 6b3b49df52..0000000000 --- a/packages/ui-modal/src/Modal/ModalBody/__tests__/ModalBody.test.tsx +++ /dev/null @@ -1,106 +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, locator, stub } from '@instructure/ui-test-utils' -import { color2hex } from '@instructure/ui-color-utils' -import { canvas } from '@instructure/ui-themes' - -import { View } from '@instructure/ui-view' -import type { ViewOwnProps } from '@instructure/ui-view' - -import { ModalBody } from '../index' -import generateComponentTheme from '../theme' - -// @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message -const ModalBodyLocator = locator(ModalBody.selector) - -describe('', async () => { - it('should render', async () => { - await mount() - const body = await ModalBodyLocator.find() - expect(body).to.exist() - }) - - it('should set inverse styles', async () => { - const variables = generateComponentTheme(canvas) - - await mount() - const body = await ModalBodyLocator.find() - - const cssStyleDeclaration = body.getComputedStyle() // CSSStyleDeclaration type - expect(variables.inverseBackground).to.equal( - color2hex(cssStyleDeclaration.getPropertyValue('background-color')) - ) - }) - - it('should set the same width and height as the parent when overflow is set to fit', async () => { - await mount( -
- -
- ) - - const body = await ModalBodyLocator.find() - - expect(window.getComputedStyle(body.getDOMNode()).width).to.equal('500px') - expect(window.getComputedStyle(body.getDOMNode()).height).to.equal('600px') - }) - - describe('when passing down props to View', async () => { - const allowedProps: Partial = { - padding: 'small', - elementRef: () => {}, - as: 'section' - } - - View.allowedProps - .filter((prop) => prop !== 'children') - .forEach((prop) => { - if (Object.keys(allowedProps).indexOf(prop) < 0) { - it(`should NOT allow the '${prop}' prop`, async () => { - const warning = `Warning: [ModalBody] prop '${prop}' is not allowed.` - const consoleError = stub(console, 'error') - const props = { - [prop]: 'foo' - } - - await mount() - expect(consoleError).to.be.calledWith(warning) - }) - } else { - it(`should allow the '${prop}' prop`, async () => { - const consoleError = stub(console, 'error') - const props = { - [prop]: allowedProps[prop] - } - - await mount() - expect(consoleError).to.not.be.called() - }) - } - }) - }) -}) diff --git a/packages/ui-modal/src/Modal/ModalFooter/__tests__/ModalFooter.test.tsx b/packages/ui-modal/src/Modal/ModalFooter/__new-tests__/ModalFooter.test.tsx similarity index 59% rename from packages/ui-modal/src/Modal/ModalFooter/__tests__/ModalFooter.test.tsx rename to packages/ui-modal/src/Modal/ModalFooter/__new-tests__/ModalFooter.test.tsx index f231b927c7..73f072d626 100644 --- a/packages/ui-modal/src/Modal/ModalFooter/__tests__/ModalFooter.test.tsx +++ b/packages/ui-modal/src/Modal/ModalFooter/__new-tests__/ModalFooter.test.tsx @@ -23,32 +23,43 @@ */ import React from 'react' -import { expect, mount, within } from '@instructure/ui-test-utils' -import { ModalFooter } from '../index' -import generateComponentTheme from '../theme' + +import { render } from '@testing-library/react' +import '@testing-library/jest-dom' + import { canvas } from '@instructure/ui-themes' import { color2hex } from '@instructure/ui-color-utils' -describe('', async () => { +import { ModalFooter } from '../index' +import generateComponentTheme from '../theme' + +const FOOTER_TEXT = 'Modal-footer-text' + +describe('', () => { it('should render', async () => { - const subject = await mount() + const { findByText } = render({FOOTER_TEXT}) + const modalFooter = await findByText(FOOTER_TEXT) - const footer = within(subject.getDOMNode()) - expect(footer).to.exist() + expect(modalFooter).toBeInTheDocument() }) it('should set inverse styles', async () => { - const variables = generateComponentTheme(canvas) - - const subject = await mount() - const footer = within(subject.getDOMNode()) + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render( + {FOOTER_TEXT} + ) + const modalFooter = await findByText(FOOTER_TEXT) - const cssStyleDeclaration = footer.getComputedStyle() // CSSStyleDeclaration type - expect(variables.inverseBackground).to.equal( - color2hex(cssStyleDeclaration.getPropertyValue('background-color')) + const modalFooterStyle = window.getComputedStyle(modalFooter) + const footerBackground = color2hex( + modalFooterStyle.getPropertyValue('background-color') ) - expect(variables.inverseBorderColor).to.equal( - color2hex(cssStyleDeclaration.getPropertyValue('border-top-color')) + const footerBorderColor = color2hex( + modalFooterStyle.getPropertyValue('border-top-color') ) + + expect(modalFooter).toBeInTheDocument() + expect(footerBackground).toBe(themeVariables.inverseBackground) + expect(footerBorderColor).toBe(themeVariables.inverseBorderColor) }) }) diff --git a/packages/ui-modal/src/Modal/ModalHeader/__new-tests__/ModalHeader.test.tsx b/packages/ui-modal/src/Modal/ModalHeader/__new-tests__/ModalHeader.test.tsx new file mode 100644 index 0000000000..3008eec72b --- /dev/null +++ b/packages/ui-modal/src/Modal/ModalHeader/__new-tests__/ModalHeader.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { canvas } from '@instructure/ui-themes' +import { color2hex } from '@instructure/ui-color-utils' + +import { ModalHeader } from '../index' +import generateComponentTheme from '../theme' + +const HEADER_TEXT = 'Modal-footer-text' + +describe('', () => { + it('should render', async () => { + const { findByText } = render({HEADER_TEXT}) + const modalHeader = await findByText(HEADER_TEXT) + + expect(modalHeader).toBeInTheDocument() + }) + + it('should set inverse styles', async () => { + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render( + {HEADER_TEXT} + ) + const modalHeader = await findByText(HEADER_TEXT) + + const modalHeaderStyle = window.getComputedStyle(modalHeader) + const headerBackground = color2hex( + modalHeaderStyle.getPropertyValue('background-color') + ) + const headerBorderColor = color2hex( + modalHeaderStyle.getPropertyValue('border-bottom-color') + ) + + expect(modalHeader).toBeInTheDocument() + expect(headerBackground).toBe(themeVariables.inverseBackground) + expect(headerBorderColor).toBe(themeVariables.inverseBorderColor) + }) + + describe('spacing prop', () => { + it('should be correct by default', async () => { + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render({HEADER_TEXT}) + const modalHeader = await findByText(HEADER_TEXT) + + const modalHeaderStyle = window.getComputedStyle(modalHeader) + const headerPadding = modalHeaderStyle.padding + + expect(modalHeader).toBeInTheDocument() + expect(headerPadding).toBe(themeVariables.padding) + }) + + it('should correctly set default spacing', async () => { + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render( + {HEADER_TEXT} + ) + const modalHeader = await findByText(HEADER_TEXT) + + const modalHeaderStyle = window.getComputedStyle(modalHeader) + const headerPadding = modalHeaderStyle.padding + + expect(modalHeader).toBeInTheDocument() + expect(headerPadding).toBe(themeVariables.padding) + }) + + it('should correctly set compact spacing', async () => { + const themeVariables = generateComponentTheme(canvas) + const { findByText } = render( + {HEADER_TEXT} + ) + const modalHeader = await findByText(HEADER_TEXT) + + const modalHeaderStyle = window.getComputedStyle(modalHeader) + const headerPadding = modalHeaderStyle.padding + + expect(modalHeader).toBeInTheDocument() + expect(headerPadding).toBe(themeVariables.paddingCompact) + }) + }) +}) diff --git a/packages/ui-modal/src/Modal/ModalHeader/__tests__/ModalHeader.test.tsx b/packages/ui-modal/src/Modal/ModalHeader/__tests__/ModalHeader.test.tsx deleted file mode 100644 index 1d44a0d92b..0000000000 --- a/packages/ui-modal/src/Modal/ModalHeader/__tests__/ModalHeader.test.tsx +++ /dev/null @@ -1,94 +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, within } from '@instructure/ui-test-utils' -import { canvas } from '@instructure/ui-themes' -import { color2hex } from '@instructure/ui-color-utils' -import { px } from '@instructure/ui-utils' - -import { ModalHeader } from '../index' -import generateComponentTheme from '../theme' - -describe('', async () => { - it('should render', async () => { - const subject = await mount() - const header = within(subject.getDOMNode()) - expect(header).to.exist() - }) - - it('should set inverse styles', async () => { - const variables = generateComponentTheme(canvas) - - const subject = await mount() - const header = within(subject.getDOMNode()) - - const cssStyleDeclaration = header.getComputedStyle() // CSSStyleDeclaration type - expect(variables.inverseBackground).to.equal( - color2hex(cssStyleDeclaration.getPropertyValue('background-color')) - ) - expect(variables.inverseBorderColor).to.equal( - color2hex(cssStyleDeclaration.getPropertyValue('border-bottom-color')) - ) - }) - - describe('spacing prop', async () => { - it('should be correct by default', async () => { - const variables = generateComponentTheme(canvas) - - const subject = await mount() - const header = within(subject.getDOMNode()) - - const cssStyleDeclaration = header.getComputedStyle() // CSSStyleDeclaration type - expect(cssStyleDeclaration.getPropertyValue('padding')).to.equal( - `${px(variables.padding)}px` - ) - }) - - it('should correctly set default spacing', async () => { - const variables = generateComponentTheme(canvas) - - const subject = await mount() - const header = within(subject.getDOMNode()) - - const cssStyleDeclaration = header.getComputedStyle() // CSSStyleDeclaration type - expect(cssStyleDeclaration.getPropertyValue('padding')).to.equal( - `${px(variables.padding)}px` - ) - }) - - it('should correctly set compact spacing', async () => { - const variables = generateComponentTheme(canvas) - - const subject = await mount() - const header = within(subject.getDOMNode()) - - const cssStyleDeclaration = header.getComputedStyle() // CSSStyleDeclaration type - expect(cssStyleDeclaration.getPropertyValue('padding')).to.equal( - `${px(variables.paddingCompact)}px` - ) - }) - }) -}) diff --git a/packages/ui-modal/src/Modal/ModalLocator.ts b/packages/ui-modal/src/Modal/ModalLocator.ts deleted file mode 100644 index a4f6e9d163..0000000000 --- a/packages/ui-modal/src/Modal/ModalLocator.ts +++ /dev/null @@ -1,47 +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 { Modal } 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 -const ModalHeaderLocator = locator(Modal.Header.selector) -// @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message -const ModalBodyLocator = locator(Modal.Body.selector) -// @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message -const ModalFooterLocator = locator(Modal.Footer.selector) - -// @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 ModalLocator = locator(Modal.selector, { - findHeader: (...args: any[]) => { - return ModalHeaderLocator.find(...args) - }, - findBody: (...args: any[]) => { - return ModalBodyLocator.find(...args) - }, - findFooter: (...args: any[]) => { - return ModalFooterLocator.find(...args) - } -}) diff --git a/packages/ui-modal/src/Modal/__new-tests__/Modal.test.tsx b/packages/ui-modal/src/Modal/__new-tests__/Modal.test.tsx new file mode 100644 index 0000000000..1ede403233 --- /dev/null +++ b/packages/ui-modal/src/Modal/__new-tests__/Modal.test.tsx @@ -0,0 +1,457 @@ +/* + * 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, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' + +import { Modal, ModalHeader, ModalBody, ModalFooter } from '../index' +import type { ModalProps } from '../props' + +describe('', () => { + afterEach(() => { + jest.clearAllMocks() + jest.restoreAllMocks() + }) + + it('should render nothing and have a node with no parent when closed', () => { + const { container } = render( + + Foo Bar Baz + + ) + expect(container.firstChild).not.toBeInTheDocument() + }) + + it('should apply theme overrides when open', async () => { + const testFont = 'test-font' + const bodyText = 'Modal-body-text' + const { findByText, findByRole } = render( + + {bodyText} + + ) + const modalBody = await findByText(bodyText) + const dialog = await findByRole('dialog') + const dialogStyle = window.getComputedStyle(dialog) + + expect(modalBody).toBeInTheDocument() + expect(dialogStyle.fontFamily).toBe(testFont) + }) + + it('should render its own positioning context if constrained to parent', async () => { + const { findByRole } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + const constrain = document.querySelector("[class*='constrainContext']") + + expect(dialog).toBeInTheDocument() + expect(constrain).toBeInTheDocument() + }) + + it("should not inherit its parent's font color", async () => { + const { findByRole } = render( +
+ + Foo Bar Baz + +
+ ) + const dialog = await findByRole('dialog') + const dialogStyle = window.getComputedStyle(dialog) + + expect(dialog).toBeInTheDocument() + expect(dialogStyle.color).toBe('rgb(0, 0, 0)') + }) + + it('should pass `as` prop to the dialog', async () => { + const { findByRole, rerender } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + expect(dialog.tagName).toBe('SPAN') + + rerender( + + Foo Bar Baz + + ) + const dialogForm = await findByRole('dialog') + + expect(dialogForm.tagName).toBe('FORM') + }) + + it('should handle null children', async () => { + const bodyText = 'Modal-body-text' + const { findByText } = render( + + {null} + {bodyText} + {null} + + ) + const modalBody = await findByText(bodyText) + + expect(modalBody).toBeInTheDocument() + }) + + it('should apply the aria attributes', async () => { + const { findByRole } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + expect(dialog).toHaveAttribute('aria-label', 'Modal Dialog') + }) + + it('should use transition', async () => { + const onEnter = jest.fn() + const onEntering = jest.fn() + const onEntered = jest.fn() + + const { findByRole } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + + await waitFor(() => { + expect(onEnter).toHaveBeenCalled() + expect(onEntering).toHaveBeenCalled() + expect(onEntered).toHaveBeenCalled() + }) + }) + + it('should support onOpen prop', async () => { + const onOpen = jest.fn() + const { findByRole } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + + await waitFor(() => { + expect(onOpen).toHaveBeenCalled() + }) + }) + + it('should support onClose prop', async () => { + const onClose = jest.fn() + + const { findByRole, rerender } = render( + + Foo Bar Baz + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + + rerender( + + Foo Bar Baz + + ) + + await waitFor(() => { + expect(onClose).toHaveBeenCalled() + }) + }) + + it('should dismiss when overlay clicked by default', async () => { + const onDismiss = jest.fn() + const { findByText } = render( + + Modal Text + + ) + const modalBody = await findByText('Modal Text') + + expect(modalBody).toBeInTheDocument() + + await waitFor(() => { + userEvent.click(document.body) + expect(onDismiss).toHaveBeenCalled() + }) + }) + + it('should NOT dismiss when overlay clicked with shouldCloseOnDocumentClick=false', async () => { + const onDismiss = jest.fn() + const onClickOuter = jest.fn() + + const { findByRole, getByTestId } = render( +
+ + + + Foo Bar Baz + + +
+ ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + + userEvent.click(getByTestId('outer-element')) + + await waitFor(() => { + expect(onClickOuter).toHaveBeenCalled() + expect(onDismiss).not.toHaveBeenCalled() + expect(dialog).toBeInTheDocument() + }) + }) + + it('should render children', async () => { + const { findByText } = render( + + + + + + ) + const cancelButton = await findByText('Cancel') + + expect(cancelButton).toBeInTheDocument() + }) + + describe('children validation', () => { + it('should pass validation when children are valid', async () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + const { findByRole } = render( + + Hello World + Foo Bar Baz + + + + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + expect(consoleErrorSpy).not.toHaveBeenCalled() + + consoleErrorSpy.mockRestore() + }) + + it('should not pass validation when children are invalid', async () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + const { findByRole } = render( + + Foo Bar Baz + + + + Hello World + + ) + const dialog = await findByRole('dialog') + const expectedErrorMessage = + 'Expected children of Modal in one of the following formats:' + + expect(dialog).toBeInTheDocument() + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + + consoleErrorSpy.mockRestore() + }) + + it('should pass inverse variant to children when set', async () => { + let headerRef: ModalHeader | null = null + let bodyRef: ModalBody | null = null + let footerRef: ModalFooter | null = null + + const { findByRole } = render( + + (headerRef = el)}>header + (bodyRef = el)}>body + (footerRef = el)}>footer + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + expect(headerRef!.props.variant).toBe('inverse') + expect(bodyRef!.props.variant).toBe('inverse') + expect(footerRef!.props.variant).toBe('inverse') + }) + + it('should pass overflow to Modal.Body', async () => { + let bodyRef: ModalBody | null = null + + const { findByRole } = render( + + (bodyRef = el)}>body + + ) + const dialog = await findByRole('dialog') + + expect(dialog).toBeInTheDocument() + expect(bodyRef!.props.overflow).toBe('fit') + }) + }) + + describe('managed focus', () => { + class ModalExample extends React.Component> { + static propTypes = { + // eslint-disable-next-line react/forbid-foreign-prop-types + ...Modal.propTypes + } + + render() { + const { label, ...props } = this.props + + return ( +
+ + + + + + + + + + + + + +
+ ) + } + } + + it('should focus closeButton by default', async () => { + const { findByText } = render() + const closeButton = await findByText('Close') + + expect(closeButton).toBeInTheDocument() + + await waitFor(() => { + expect(document.activeElement).toBe(closeButton) + }) + }) + + it('should take a prop for finding default focus', async () => { + const { findByTestId } = render( + document.getElementById('input-one')} + /> + ) + const input = await findByTestId('input-first') + + await waitFor(() => { + expect(input).toHaveFocus() + }) + }) + }) +}) diff --git a/packages/ui-modal/src/Modal/__tests__/Modal.test.tsx b/packages/ui-modal/src/Modal/__tests__/Modal.test.tsx deleted file mode 100644 index 20bc9a1c62..0000000000 --- a/packages/ui-modal/src/Modal/__tests__/Modal.test.tsx +++ /dev/null @@ -1,505 +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, { useState } from 'react' -import { - expect, - match, - mount, - stub, - wait, - within -} from '@instructure/ui-test-utils' - -import { Modal, ModalHeader, ModalBody, ModalFooter } from '../index' -import { ModalLocator } from '../ModalLocator' -import type { ModalProps } from '../props' - -describe('', async () => { - it('should render nothing and have a node with no parent when closed', async () => { - await mount( - - Foo Bar Baz - - ) - const modal = await ModalLocator.find(':label(Modal Dialog)', { - expectEmpty: true - }) - - expect(modal).to.not.exist() - }) - - it('should apply theme overrides when open', async () => { - await mount( - - Foo Bar Baz - - ) - - const modal = await ModalLocator.find() - const body = await modal.findBody() - - await wait(() => { - expect(body.getComputedStyle().width).to.equal('158px') // subtract the borders - }) - }) - - it('should render its own positioning context if constrained to parent', async () => { - await mount( - - Foo Bar Baz - - ) - - const modal = await ModalLocator.find(':label(Modal Dialog)') - const constrain = await modal.find('[class*="-modal__constrainContext"]') - - expect(constrain).to.exist() - }) - - it("should not inherit its parent's font color", async () => { - await mount( -
- - Foo Bar Baz - -
- ) - - const modal = await ModalLocator.find() - const body = await modal.findBody() - - expect(body.getComputedStyle().color).to.equal('rgb(0, 0, 0)') - }) - - it('should pass `as` prop to the dialog', async () => { - const subject = await mount( - - Foo Bar Baz - - ) - const modal = await ModalLocator.find() - let dialog = await modal.find('[role="dialog"]') - - expect(dialog.getTagName()).to.equal('span') - - await subject.setProps({ as: 'form' }) - - dialog = await modal.find('[role="dialog"]') - expect(dialog.getTagName()).to.equal('form') - }) - - it('should handle null children', async () => { - await mount( - - {null} - Foo Bar Baz - {null} - - ) - const modal = await ModalLocator.find() - - expect(modal).to.exist() - }) - - it('should apply the aria attributes', async () => { - await mount( - - Foo Bar Baz - - ) - const modal = await ModalLocator.find() - const dialog = await modal.find(':label(Modal Dialog)') - - expect(dialog.getAttribute('role')).to.equal('dialog') - }) - - it('should use transition', async () => { - const onEnter = stub() - - const onEntering = stub() - - const onEntered = stub() - - await mount( - - Foo Bar Baz - - ) - - await wait(() => { - expect(onEnter).to.have.been.called() - expect(onEntering).to.have.been.called() - expect(onEntered).to.have.been.called() - }) - }) - - it('should support onOpen prop', async () => { - const onOpen = stub() - await mount( - - Foo Bar Baz - - ) - - await wait(() => { - expect(onOpen).to.have.been.called() - }) - }) - - it('should support onClose prop', async () => { - const onClose = stub() - - const subject = await mount( - - Foo Bar Baz - - ) - - await subject.setProps({ open: false }) - - await wait(() => { - expect(onClose).to.have.been.called() - }) - }) - - // TODO: this test works locally but fails in CI so it's skipped for now - // should be turned back on when these tests are moved to the new format (jest + testing library) - it.skip('should dismiss when overlay clicked by default', async () => { - const onDismiss = stub() - await mount( - - - Foo Bar Baz - - - ) - - const modal = await ModalLocator.find() - - await wait(() => { - expect(modal.containsFocus()).to.be.true() - }) - - await (within(modal.getOwnerDocument().documentElement) as any).click() - - await wait(() => { - expect(onDismiss).to.have.been.called() - }) - }) - - it('should NOT dismiss when overlay clicked with shouldCloseOnDocumentClick=false', async () => { - const onDismiss = stub() - await mount( - - - Foo Bar Baz - - - ) - - const modal = await ModalLocator.find() - - await wait(() => { - expect(modal.containsFocus()).to.be.true() - }) - - await (within(modal.getOwnerDocument().documentElement) as any).click() - - await wait(() => { - expect(onDismiss).to.not.have.been.called() - }) - - expect(modal).to.exist() - }) - - it('should render children', async () => { - await mount( - - - - - - ) - const modal = await ModalLocator.find(':label(Modal Dialog)') - const cancelButton = await modal.find(':label(Cancel)') - - expect(cancelButton).to.exist() - }) - - describe('children validation', async () => { - it('should pass validation when children are valid', async () => { - await expect( - mount( - - Hello World - Foo Bar Baz - - - - - ) - ).to.not.be.rejected() - }) - - it('should not pass validation when children are invalid', async () => { - const consoleError = stub(console, 'error') - - await mount( - - Foo Bar Baz - - - - Hello World - - ) - expect(consoleError).to.have.been.calledWithMatch( - match.string, - match.string, - 'Expected children of Modal in one of the following formats:' - ) - }) - - it('should pass inverse variant to children when set', async () => { - let headerRef: ModalHeader | null = null - let bodyRef: ModalBody | null = null - let footerRef: ModalFooter | null = null - - await mount( - - (headerRef = el)}> - Hello Dark World - - (bodyRef = el)}>Foo Bar Baz - (footerRef = el)}> - - - - ) - - expect(headerRef!.props.variant).to.equal('inverse') - expect(bodyRef!.props.variant).to.equal('inverse') - expect(footerRef!.props.variant).to.equal('inverse') - }) - - it('should pass overflow to Modal.Body', async () => { - let bodyRef: ModalBody | null = null - - await mount( - - (bodyRef = el)}>Foo Bar Baz - - ) - expect(bodyRef!.props.overflow).to.equal('fit') - }) - }) - - describe('managed focus', async () => { - class ModalExample extends React.Component> { - static propTypes = { - // eslint-disable-next-line react/forbid-foreign-prop-types - ...Modal.propTypes - } - - render() { - const { label, ...props } = this.props - - return ( -
- - - - - - - - - - - - - -
- ) - } - } - - it('should focus closeButton by default', async () => { - await mount() - - const modal = await ModalLocator.find(':label(A Modal)') - const closeButton = await modal.find(':label(Close)') - - await wait(() => { - expect(closeButton.focused()).to.be.true() - }) - }) - - it('should take a prop for finding default focus', async () => { - await mount( - { - return document.getElementById('input-one') - }} - /> - ) - - const modal = await ModalLocator.find(':label(A Modal)') - const input = await modal.find('#input-one') - - await wait(() => { - expect(input.focused()).to.be.true() - }) - }) - - it('should call onDismiss prop when Esc key pressed by default', async () => { - const onDismiss = stub() - - await mount( - { - return document.getElementById('input-one') - }} - /> - ) - - const modal = await ModalLocator.find() - - await wait(() => { - expect(modal.containsFocus()).to.be.true() - }) - - await (within(modal.getOwnerDocument().documentElement) as any).keyUp( - 'escape', - null, - { focusable: false } - ) - - await wait(() => { - expect(onDismiss).to.have.been.called() - }) - }) - }) - - // TODO: this test works locally but fails in CI so it's skipped for now - // should be turned back on when these tests are moved to the new format (jest + testing library) - it.skip('should not call stale callbacks', async () => { - function Example(props: { handleDissmiss: (v: number) => number }) { - const [value, setValue] = useState(0) - - function onButtonClick() { - setValue(value + 1) - } - - return ( - { - props.handleDissmiss(value) - }} - > - -
{value}
- -
-
- ) - } - const handleDissmiss = stub() - - await mount() - - const modal = await ModalLocator.find() - - const incrementBtn: HTMLElement = document.querySelector('#increment-btn')! - const btn = within(incrementBtn) - - await btn.click() - - // to trigger the modal to close - await (within(modal.getOwnerDocument().documentElement) as any).click() - - expect(handleDissmiss).to.have.been.calledWith(1) - }) -}) diff --git a/packages/ui-modal/src/Modal/locator.ts b/packages/ui-modal/src/Modal/locator.ts deleted file mode 100644 index e6ebcdccea..0000000000 --- a/packages/ui-modal/src/Modal/locator.ts +++ /dev/null @@ -1,28 +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 { ModalLocator } from './ModalLocator' - -export { ModalLocator } -export default ModalLocator diff --git a/packages/ui-modal/tsconfig.build.json b/packages/ui-modal/tsconfig.build.json index 08acda20e4..4a84265ab5 100644 --- a/packages/ui-modal/tsconfig.build.json +++ b/packages/ui-modal/tsconfig.build.json @@ -24,8 +24,6 @@ { "path": "../ui-babel-preset/tsconfig.build.json" }, { "path": "../ui-color-utils/tsconfig.build.json" }, { "path": "../ui-position/tsconfig.build.json" }, - { "path": "../ui-test-locator/tsconfig.build.json" }, - { "path": "../ui-test-utils/tsconfig.build.json" }, { "path": "../ui-themes/tsconfig.build.json" } ] } diff --git a/packages/ui-popover/src/Popover/__new-tests__/Popover.test.tsx b/packages/ui-popover/src/Popover/__new-tests__/Popover.test.tsx index c4a531c0cf..143b0d9488 100644 --- a/packages/ui-popover/src/Popover/__new-tests__/Popover.test.tsx +++ b/packages/ui-popover/src/Popover/__new-tests__/Popover.test.tsx @@ -95,7 +95,7 @@ describe('', () => { ) - act(async () => { + await act(async () => { await userEvent.click(document.body) }) await waitFor(() => {