From dad56ed475d76d940b8933265a17209e1868b5fd Mon Sep 17 00:00:00 2001 From: Matyas Forian-Szabo Date: Tue, 28 Jul 2026 14:56:33 +0200 Subject: [PATCH 1/3] build: unify the IDE and build tsconfigs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root tsconfig.json (which every packages/*/tsconfig.json extends, and which the IDE resolves for test files) used module Node16 / moduleResolution node16, while tsconfig.build.json (used by ts:check and build:types) used ESNext / bundler. Under node16 TS classifies every file as CommonJS, since no package sets "type": "module", so the browser-mode tests importing ESM-only vitest-browser-react and vitest/browser reported TS1479 in the editor while ts:check stayed green. Make tsconfig.json extend tsconfig.build.json so the two cannot drift again. Only exclude is restated, since it replaces rather than merges. This clears 31 TS1479s plus ~90 related interop errors (TS2835, TS1541, TS2834) across the 15 browser-mode test files and packages/__docs__. The bundler model is the accurate one here: these tests are never compiled by tsc, Vite transforms them as ESM. Also drop the __tests__ exclude from the six tsconfig.build.json files that had it, so their tests are typechecked like the other 89 packages'. That exclude was hiding a real error in FocusRegion.test.tsx, where .element() returns HTMLElement | SVGElement; cast it to HTMLElement. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- packages/debounce/tsconfig.build.json | 1 - .../src/__tests__/FocusRegion.test.tsx | 2 +- packages/ui-a11y-utils/tsconfig.build.json | 1 - packages/ui-dom-utils/tsconfig.build.json | 1 - packages/ui-theme-tokens/tsconfig.build.json | 1 - packages/ui-utils/tsconfig.build.json | 1 - packages/uid/tsconfig.build.json | 1 - tsconfig.json | 28 ++----------------- 8 files changed, 3 insertions(+), 33 deletions(-) diff --git a/packages/debounce/tsconfig.build.json b/packages/debounce/tsconfig.build.json index d913175169..3deeb236ec 100644 --- a/packages/debounce/tsconfig.build.json +++ b/packages/debounce/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../ui-babel-preset/tsconfig.build.json" diff --git a/packages/ui-a11y-utils/src/__tests__/FocusRegion.test.tsx b/packages/ui-a11y-utils/src/__tests__/FocusRegion.test.tsx index 522c8d063e..1082018c98 100644 --- a/packages/ui-a11y-utils/src/__tests__/FocusRegion.test.tsx +++ b/packages/ui-a11y-utils/src/__tests__/FocusRegion.test.tsx @@ -40,7 +40,7 @@ describe('FocusRegion', () => { ) - container = page.getByTestId('container').element() + container = page.getByTestId('container').element() as HTMLElement }) afterEach(() => { diff --git a/packages/ui-a11y-utils/tsconfig.build.json b/packages/ui-a11y-utils/tsconfig.build.json index 7d54d18e6a..8e0b5aeb55 100644 --- a/packages/ui-a11y-utils/tsconfig.build.json +++ b/packages/ui-a11y-utils/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../console/tsconfig.build.json" }, { "path": "../ui-a11y-content/tsconfig.build.json" }, diff --git a/packages/ui-dom-utils/tsconfig.build.json b/packages/ui-dom-utils/tsconfig.build.json index 34e9bdac72..9f56a996ce 100644 --- a/packages/ui-dom-utils/tsconfig.build.json +++ b/packages/ui-dom-utils/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../ui-babel-preset/tsconfig.build.json" }, { "path": "../console/tsconfig.build.json" }, diff --git a/packages/ui-theme-tokens/tsconfig.build.json b/packages/ui-theme-tokens/tsconfig.build.json index c8f73fb00f..3bde7b593c 100644 --- a/packages/ui-theme-tokens/tsconfig.build.json +++ b/packages/ui-theme-tokens/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../ui-babel-preset/tsconfig.build.json" diff --git a/packages/ui-utils/tsconfig.build.json b/packages/ui-utils/tsconfig.build.json index 26a05380fd..53018918ae 100644 --- a/packages/ui-utils/tsconfig.build.json +++ b/packages/ui-utils/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../ui-babel-preset/tsconfig.build.json" diff --git a/packages/uid/tsconfig.build.json b/packages/uid/tsconfig.build.json index d913175169..3deeb236ec 100644 --- a/packages/uid/tsconfig.build.json +++ b/packages/uid/tsconfig.build.json @@ -6,7 +6,6 @@ "rootDir": "./src" }, "include": ["src"], - "exclude": ["src/**/__tests__/**/*"], "references": [ { "path": "../ui-babel-preset/tsconfig.build.json" diff --git a/tsconfig.json b/tsconfig.json index 70e26a7989..41a7d51002 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,8 @@ { - "compilerOptions": { - "pretty": false, - "module": "Node16", - "moduleResolution": "node16", - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "experimentalDecorators": true, - "allowJs": true, - "isolatedModules": true, - "jsx": "react-jsx", - "jsxImportSource": "@emotion/react", - "target": "ES6", - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "checkJs": false, - "noImplicitAny": true, - "strict": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "lib": ["ES2023", "DOM", "DOM.Iterable"], - "allowSyntheticDefaultImports": true - }, + "extends": "./tsconfig.build.json", "exclude": [ "regression-test", + "cypress.config.ts", "cypress", "node_modules", "**/__testfixtures__" From 16ff039dacd98ff4201a1d13241d8b0d52cdd11a Mon Sep 17 00:00:00 2001 From: Matyas Forian-Szabo Date: Wed, 29 Jul 2026 15:05:00 +0200 Subject: [PATCH 2/3] test(many): convert more component tests to vitest browser mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the unit tests of ui-billboard, ui-breadcrumb, ui-buttons, ui-byline, ui-calendar, ui-checkbox and ui-color-picker to Vitest browser mode, and port the matching Cypress specs (ToggleButton, ColorIndicator, ColorMixer, ColorPicker, ColorPreset) to the bottom of the corresponding unit test files. The Cypress specs are removed. Notable real-browser adaptations: - `{ force: true }` where Playwright refuses to click disabled/aria-disabled or visually hidden elements - native `ClipboardEvent` with a real `DataTransfer` instead of `fireEvent.paste` - `fireEvent.focusIn/focusOut` instead of `blur`/`focus`, which React does not listen for - hidden tooltips are queried from the DOM, since they are not in the accessibility tree - `unhover` before hover/click assertions, because the pointer position persists between tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- cypress/component/ColorIndicator.cy.tsx | 112 -- cypress/component/ColorMixer.cy.tsx | 1177 ----------------- cypress/component/ColorPicker.cy.tsx | 524 -------- cypress/component/ColorPreset.cy.tsx | 192 --- cypress/component/ToggleButton.cy.tsx | 68 - .../Billboard/__tests__/Billboard.test.tsx | 77 +- .../Breadcrumb/__tests__/Breadcrumb.test.tsx | 30 +- .../__tests__/BreadcrumbLink.test.tsx | 48 +- .../BaseButton/__tests__/BaseButton.test.tsx | 202 +-- .../src/Button/__tests__/Button.test.tsx | 217 +-- .../__tests__/CloseButton.test.tsx | 16 +- .../__tests__/CondensedButton.test.tsx | 82 +- .../IconButton/__tests__/IconButton.test.tsx | 86 +- .../__tests__/ToggleButton.test.tsx | 114 +- .../src/Byline/__tests__/Byline.test.tsx | 37 +- .../src/Calendar/__tests__/Calendar.test.tsx | 117 +- .../src/Calendar/__tests__/Day.test.tsx | 56 +- .../src/Checkbox/__tests__/Checkbox.test.tsx | 143 +- .../__tests__/CheckboxFacade.test.tsx | 16 +- .../Checkbox/__tests__/ToggleFacade.test.tsx | 14 +- .../__tests__/CheckboxGroup.test.tsx | 87 +- .../__tests__/ColorContrast.test.tsx | 76 +- .../__tests__/ColorIndicator.test.tsx | 80 +- .../ColorMixer/__tests__/ColorMixer.test.tsx | 789 +++++++++-- .../__tests__/ColorPicker.test.tsx | 785 ++++++++--- .../__tests__/ColorPreset.test.tsx | 201 ++- vitest.config.mts | 14 + 27 files changed, 2343 insertions(+), 3017 deletions(-) delete mode 100644 cypress/component/ColorIndicator.cy.tsx delete mode 100644 cypress/component/ColorMixer.cy.tsx delete mode 100644 cypress/component/ColorPicker.cy.tsx delete mode 100644 cypress/component/ColorPreset.cy.tsx delete mode 100644 cypress/component/ToggleButton.cy.tsx diff --git a/cypress/component/ColorIndicator.cy.tsx b/cypress/component/ColorIndicator.cy.tsx deleted file mode 100644 index 707c7f9bec..0000000000 --- a/cypress/component/ColorIndicator.cy.tsx +++ /dev/null @@ -1,112 +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 { ColorIndicator } from '@instructure/ui/latest' - -import '../support/component' -import 'cypress-real-events' - -import { colorToRGB, colorToHex8 } from '@instructure/ui-color-utils' - -const colorTestCases = { - '3 digit hex': '#069', - '6 digit hex': '#01659a', - rgb: 'rgb(100, 0, 200)', - rgba: 'rgba(100, 0, 200, .5)', - named: 'white', - hsl: 'hsl(30, 100%, 50%)', - hsla: 'hsla(30, 100%, 50%, .35)' -} - -describe('', () => { - it('should display empty by default', () => { - cy.mount() - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .and('have.css', 'box-shadow', 'none') - }) - - Object.entries(colorTestCases).forEach(([testCase, testColor]) => { - it(`should display ${testCase} color`, () => { - const expectedColor = colorToRGB(testColor) - - cy.mount() - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - }) - - // needs to be checked separately, the alpha is rounded different - it('should display 8 digit hexa color', () => { - const testColor = '#06AD8580' - const expectedColor = colorToHex8(testColor) - - cy.mount() - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToHex8(colorValue)).to.deep.equal(expectedColor) - }) - }) - - it('should display circle by default', () => { - cy.mount() - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', ['border-radius', 'width', 'height']) - .then((styles) => { - const { 'border-radius': borderRadius, width, height } = styles - - expect(width).to.equal(height) - expect(borderRadius).to.equal(width) - }) - }) - - it('should display rectangle version', () => { - cy.mount() - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', ['border-radius', 'width', 'height']) - .then((styles) => { - const { 'border-radius': borderRadius, width, height } = styles - - expect(width).to.equal(height) - expect(borderRadius).to.equal('6px') - }) - }) -}) diff --git a/cypress/component/ColorMixer.cy.tsx b/cypress/component/ColorMixer.cy.tsx deleted file mode 100644 index b6c0072a1b..0000000000 --- a/cypress/component/ColorMixer.cy.tsx +++ /dev/null @@ -1,1177 +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 { ColorMixer } from '@instructure/ui/latest' - -import '../support/component' -import 'cypress-real-events' - -import { colorToHex8 } from '@instructure/ui-color-utils' - -const testValue = { - value: '#09918B' -} - -const testInputLabels = { - rgbRedInputScreenReaderLabel: 'Input field for red', - rgbGreenInputScreenReaderLabel: 'Input field for green', - rgbBlueInputScreenReaderLabel: 'Input field for blue', - rgbAlphaInputScreenReaderLabel: 'Input field for alpha' -} - -const testScreenReaderLabels = { - colorSliderNavigationExplanationScreenReaderLabel: `You are on a color slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`, - alphaSliderNavigationExplanationScreenReaderLabel: `You are on an alpha slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`, - colorPaletteNavigationExplanationScreenReaderLabel: `You are on a color palette. To navigate on the palette up, left, down or right, use the 'W', 'A', 'S' and 'D' buttons respectively` -} - -describe('', () => { - it('should change hue value when click at the middle of the slider', () => { - const onChange = cy.spy() - cy.mount( - - ) - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width / 2, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should change hue value when click at the end of the slider', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - - cy.wrap($colorSlider).realClick({ x: rect.width - 1, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should change hue value when click at the beginning of the slider', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - - cy.wrap($colorSlider).realClick({ x: 1, y: rect.height / 2 }) - }) - // Because we already passed the `value` that different from the default value then the component changes their color once, so if we want to test with any action after that, `onChange` should be called twice. - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should hue value change with 'a' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('a') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should hue value change with 'd' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('d') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should hue value change with 'ArrowLeft' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowLeft') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should hue value change with 'ArrowRight' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowRight') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it('the hue indicator move left', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .as('hueSlider') - .find('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - - cy.realPress('a').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.be.lessThan(pos1) - - cy.realPress('ArrowLeft').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.be.lessThan(pos2) - }) - }) - }) - }) - - it('the hue indicator move right', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .as('hueSlider') - .find('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - - cy.realPress('d').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.be.greaterThan(pos1) - - cy.realPress('ArrowRight').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.be.greaterThan(pos2) - }) - }) - }) - }) - - it('should not move the hue indicator when reach the left border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ) - .as('hueSlider') - .find('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - - cy.realPress('a').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowLeft').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos2) - }) - }) - }) - }) - - it('should not move the hue indicator when reach the right border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` - ).as('hueSlider') - - // Initial positioning to reach the right end of the slider - cy.get('@hueSlider').focus() - cy.realPress('ArrowRight') - cy.realPress('ArrowRight') - - cy.get('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - cy.realPress('d').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowRight').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos2) - }) - }) - }) - }) - - it('should change alpha value when click at the beginning of the bar', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - - cy.wrap($colorSlider).realClick({ x: 1, y: rect.height / 2 }) - }) - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it('should change alpha value when click at the end of the bar', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - - cy.wrap($colorSlider).realClick({ x: rect.width - 1, y: rect.height / 2 }) - }) - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it('should change alpha value when click at the middle of the slider', () => { - const onChange = cy.spy() - cy.mount( - - ) - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width / 2, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should alpha value change with 'ArrowRight' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowRight') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should alpha value change with 'ArrowLeft' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowLeft') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should alpha value change with 'a' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('a') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should alpha value change with 'd' key pressed`, () => { - const onChange = cy.spy() - - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('d') - - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it('should not move the alpha indicator when reach the left border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ) - .as('hueSlider') - .find('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - - cy.realPress('a').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowLeft').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos2) - }) - }) - }) - }) - - it('should not move the alpha indicator when reach the right border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` - ).as('hueSlider') - - cy.get('[class*=-colorMixerSlider__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@hueSlider').focus() - cy.realPress('d').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowRight').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos2) - }) - }) - }) - }) - - it('should palette change the color when mousedown event is received inside the palette', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width / 2, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.calledWith', '#783A3AFF') - }) - - it('should palette change the color when mousedown event is received at the top border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - - cy.wrap($colorSlider).realClick({ x: rect.width / 2, y: 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the bottom border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width / 2, y: rect.height - 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the left border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: 1, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the right border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width - 1, y: rect.height / 2 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the top left corner', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - cy.wrap($colorSlider).realClick({ x: 1, y: 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the top right corner', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width - 1, y: 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the bottom right corner', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: rect.width - 2, y: rect.height - 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it('should palette change the color when mousedown event is received at the bottom left corner', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ).then(($colorSlider) => { - const rect = $colorSlider[0].getBoundingClientRect() - cy.wrap($colorSlider).realClick({ x: 2, y: rect.height - 1 }) - }) - - cy.wrap(onChange).should('have.been.called') - }) - - it(`should onChange is call when the palette receive event from keyboard 'a'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('a') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'w'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('w') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 's'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('s') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'd'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('d') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'ArrowLeft'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowLeft') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'ArrowRight'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowRight') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'ArrowUp'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowUp') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it(`should onChange is call when the palette receive event from keyboard 'ArrowDown'`, () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .focus() - .realPress('ArrowDown') - - // use `calledTwice` because it is called first time when passing `testValue` color - cy.wrap(onChange).should('have.been.calledTwice') - }) - - it('should palette indicator moves up when receive the ArrowUp or w keyboard event', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().y - - cy.get('@palette').focus() - - cy.realPress('w').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().y - expect(pos2).to.be.lessThan(pos1) - - cy.realPress('ArrowUp').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().y - expect(pos3).to.be.lessThan(pos2) - }) - }) - }) - }) - - it('should palette indicator moves down when receive the ArrowDown or s keyboard event', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().y - - cy.get('@palette').focus() - - cy.realPress('s').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().y - expect(pos2).to.be.greaterThan(pos1) - - cy.realPress('ArrowDown').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().y - expect(pos3).to.be.greaterThan(pos2) - }) - }) - }) - }) - - it('should palette indicator moves left when receive the ArrowLeft or a keyboard event', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@palette').focus() - - cy.realPress('a').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.be.lessThan(pos1) - - cy.realPress('ArrowLeft').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.be.lessThan(pos2) - }) - }) - }) - }) - - it('should palette indicator moves right when receive the ArrowRight or d keyboard event', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@palette').focus() - - cy.realPress('d').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.be.greaterThan(pos1) - - cy.realPress('ArrowRight').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.be.greaterThan(pos2) - }) - }) - }) - }) - - it('should palette indicator does not move up when it reach the top border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().y - - cy.get('@palette').focus() - - cy.realPress('w').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().y - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowUp').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().y - expect(pos3).to.equal(pos1) - }) - }) - }) - }) - - it('should palette indicator does not move down when it reach the bottom border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().y - - cy.get('@palette').focus() - - cy.realPress('s').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().y - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowDown').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().y - expect(pos3).to.equal(pos1) - }) - }) - }) - }) - - it('should palette indicator does not move left when it reach the left border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@palette').focus() - - cy.realPress('a').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowLeft').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos1) - }) - }) - }) - }) - - it('should palette indicator does not move right when it reach the right border', () => { - const onChange = cy.spy() - cy.mount( - - ) - - cy.get( - `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` - ) - .as('palette') - .find('[class*=-ColorPalette__indicator]') - .as('indicator') - .then(($indicator) => { - const pos1 = $indicator[0].getBoundingClientRect().x - - cy.get('@palette').focus() - - cy.realPress('d').then(() => { - const pos2 = $indicator[0].getBoundingClientRect().x - expect(pos2).to.equal(pos1) - - cy.realPress('ArrowRight').then(() => { - const pos3 = $indicator[0].getBoundingClientRect().x - expect(pos3).to.equal(pos1) - }) - }) - }) - }) -}) diff --git a/cypress/component/ColorPicker.cy.tsx b/cypress/component/ColorPicker.cy.tsx deleted file mode 100644 index 47399af98f..0000000000 --- a/cypress/component/ColorPicker.cy.tsx +++ /dev/null @@ -1,524 +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 { - ColorPicker, - ColorMixer, - ColorPreset, - ColorContrast, - Button -} from '@instructure/ui/latest' - -import '../support/component' -import 'cypress-real-events' - -import { colorToRGB, color2hex } from '@instructure/ui-color-utils' - -// Resolve the RGBA from its (screen-reader) label via the label's -// `for` association, instead of relying on the TextInput's internal DOM -// nesting (which changed and broke `label.siblings('span').find('input')`). -const rgbaInput = (labelText: string) => - cy - .contains('label', labelText) - .invoke('attr', 'for') - .then((id) => cy.get(`#${id}`)) - -const colorPreset = [ - '#ffffff', - '#0CBF94', - '#0C89BF', - '#BF0C6D', - '#BF8D0C', - '#ff0000', - '#576A66', - '#35423A', - '#35423F' -] - -const SimpleExample = (props) => { - return ( - - ) -} - -describe('', () => { - it('should display the color which was typed in simple input mode', () => { - const testColor = '0CBF2D' - const expectedColor = colorToRGB(testColor) - - cy.mount() - - cy.get('input[id^="TextInput_"]').type(testColor) - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - - it('should display the color in the trigger button in complex mode', () => { - const testColor = '0374B5' - const expectedColor = colorToRGB(testColor) - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').type(testColor) - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - - it('should display the list of colors passed to it in complex mode', () => { - cy.mount( - - ) - cy.get('button').click() - - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .should('have.length', colorPreset.length) - .each(($indicator, index) => { - cy.wrap($indicator) - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const expectedColor = colorToRGB(colorPreset[index]) - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - }) - - it('should correctly set the color when picked from the list of colors in complex mode', () => { - cy.mount( - - ) - cy.get('button').click() - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(1) - .realClick() - cy.contains('button', 'add').realClick() - - cy.get('input[type="text"]').should( - 'have.value', - colorPreset[1].substring(1) - ) - }) - - it('should correctly call onChange with the color when picked from the list of colors in complex mode', () => { - const onChange = cy.spy() - cy.mount( - - ) - cy.get('button').click() - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(1) - .realClick() - cy.contains('button', 'add').realClick() - - cy.wrap(onChange).should('have.been.calledWith', colorPreset[1]) - }) - - it('should display the text passed to ColorContrast in complex mode', () => { - cy.mount( - - ) - cy.get('button').realClick() - - cy.get('div[class$="-colorContrast"]') - .should('contain', 'Normal text') - .and('contain', 'Large text') - .and('contain', 'Graphics text') - }) - - it('should display the correct color in the colormixer when the input is prefilled in custom popover mode', () => { - const testColor = '0374B5' - const expectedColor = colorToRGB(`#${testColor}`) - - cy.mount( - - {(value, onChange, handleAdd, handleClose) => ( -
- -
- - -
-
- )} -
- ) - cy.get('input[id^="TextInput_"]').type(testColor) - cy.get('button').realClick() - - rgbaInput('Input field for red').should('have.value', expectedColor.r) - rgbaInput('Input field for green').should('have.value', expectedColor.g) - rgbaInput('Input field for blue').should('have.value', expectedColor.b) - rgbaInput('Input field for alpha').should('have.value', '100') - }) - - it('should trigger onChange when selected color is added from colorMixer in custom popover mode', () => { - const onChange = cy.spy() - const rgb = { r: 131, g: 6, b: 25, a: 1 } - - cy.mount( - - {(value, onChange, handleAdd, handleClose) => { - return ( -
- -
- - -
-
- ) - }} -
- ) - cy.get('button').click() - - rgbaInput('Input field for red').type(rgb.r.toString()) - rgbaInput('Input field for green').type(rgb.g.toString()) - rgbaInput('Input field for blue').type(rgb.b.toString()) - - cy.contains('button', 'add').realClick() - cy.wrap(onChange).should('have.been.calledWith', color2hex(rgb)) - }) - - it('should display the color in the trigger button in custom popover mode', () => { - const testColor = '0374B5' - const expectedColor = colorToRGB(testColor) - - cy.mount( - - {(value, onChange, handleAdd, handleClose) => ( -
- -
- - -
-
- )} -
- ) - - cy.get('input[id^="TextInput_"]').type(testColor) - - cy.get('div[class$="-colorIndicator"]') - .should('exist') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - - it('should display the list of colors passed to it in custom popover mode', () => { - cy.mount( - - {(value, onChange, handleAdd, handleClose) => ( -
- -
- - -
-
- )} -
- ) - cy.get('button').realClick() - - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .should('have.length', colorPreset.length) - .each(($indicator, index) => { - cy.wrap($indicator) - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const expectedColor = colorToRGB(colorPreset[index]) - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - }) - - it('should correctly set the color when picked from the list of colors in custom popover mode', () => { - cy.mount( - - {(value, onChange, handleAdd, handleClose) => ( -
- -
- - -
-
- )} -
- ) - cy.get('button').click() - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(3) - .realClick() - cy.contains('button', 'add').realClick() - - cy.get('input[type="text"]').should( - 'have.value', - colorPreset[3].substring(1) - ) - }) - - it('should correctly call onChange with the color when picked from the list of colors in custom popover mode', () => { - const onChange = cy.spy() - - cy.mount( - - {(value, onChange, handleAdd, handleClose) => { - return ( -
- -
- - -
-
- ) - }} -
- ) - cy.get('button').click() - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(3) - .realClick() - cy.contains('button', 'add').realClick() - - cy.wrap(onChange).should('have.been.calledWith', colorPreset[3]) - }) - - it('should display the text passed to ColorContrast in custom popover mode', () => { - cy.mount( - - {(value, onChange, handleAdd, handleClose) => ( -
- - -
- - -
-
- )} -
- ) - cy.get('button').realClick() - - cy.get('div[class$="-colorContrast"]') - .should('contain', 'Normal text') - .and('contain', 'Large text') - .and('contain', 'Graphics text') - }) -}) diff --git a/cypress/component/ColorPreset.cy.tsx b/cypress/component/ColorPreset.cy.tsx deleted file mode 100644 index 617e3e42fe..0000000000 --- a/cypress/component/ColorPreset.cy.tsx +++ /dev/null @@ -1,192 +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 { ColorPreset } from '@instructure/ui/latest' - -import '../support/component' -import 'cypress-real-events' - -import { colorToRGB } from '@instructure/ui-color-utils' - -const testValue = { - colors: [ - '#ffffff', - '#0CBF94', - '#0C89BF00', - '#BF0C6D', - '#BF8D0C', - '#ff0000', - '#576A66', - '#35423A', - '#35423F' - ], - onSelect: () => {} -} - -const testColorMixerSettings = { - addNewPresetButtonScreenReaderLabel: 'Add new preset button label', - selectColorLabel: 'Select', - removeColorLabel: 'Remove', - onPresetChange: () => {}, - popoverAddButtonLabel: 'Add', - popoverCloseButtonLabel: 'Cancel', - colorMixer: { - rgbRedInputScreenReaderLabel: 'Input field for red', - rgbGreenInputScreenReaderLabel: 'Input field for green', - rgbBlueInputScreenReaderLabel: 'Input field for blue', - rgbAlphaInputScreenReaderLabel: 'Input field for alpha', - colorSliderNavigationExplanationScreenReaderLabel: `You are on a color slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`, - alphaSliderNavigationExplanationScreenReaderLabel: `You are on an alpha slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`, - colorPaletteNavigationExplanationScreenReaderLabel: `You are on a color palette. To navigate on the palette up, left, down or right, use the 'W', 'A', 'S' and 'D' buttons respectively` - }, - colorContrast: { - firstColor: '#FF0000', - label: 'Color Contrast Ratio', - successLabel: 'PASS', - failureLabel: 'FAIL', - normalTextLabel: 'Normal text', - largeTextLabel: 'Large text', - graphicsTextLabel: 'Graphics text', - firstColorLabel: 'Background', - secondColorLabel: 'Foreground' - } -} - -describe('', () => { - it('should display color indicators for all colors', () => { - cy.mount() - - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .should('have.length', testValue.colors.length) - .each(($indicator, index) => { - cy.wrap($indicator) - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const expectedColor = colorToRGB(testValue.colors[index]) - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - }) - - it('empty string should leave all unselected', () => { - cy.mount() - - cy.get('button[aria-label="selected"]').should('not.exist') - cy.get('div[class$="__selectedIndicator"]').should('not.exist') - }) - - it('should select proper color', () => { - const testableColor = testValue.colors[6] - cy.mount() - - cy.get('[class*="selectedIndicator"]') - .closest('button') - .within(() => { - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .invoke('css', 'box-shadow') - .then((boxShadow) => { - const expectedColor = colorToRGB(testableColor) - const colorValue = boxShadow.toString().split(')')[0] + ')' - - expect(colorToRGB(colorValue)).to.deep.equal(expectedColor) - }) - }) - }) - - it('shows menu on indicator click', () => { - cy.mount( - - ) - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(5) - .realClick() - cy.get('div[id^=DrilldownHeader-Title]').should( - 'contain', - testValue.colors[5] - ) - cy.get('div[role="menu"]') - .should('contain', testColorMixerSettings.selectColorLabel) - .and('contain', testColorMixerSettings.removeColorLabel) - }) - - it('should allow adding presets', () => { - const onPresetChange = cy.spy() - cy.mount( - - ) - cy.get('div[class$="addNewPresetButton"]').realClick() - - cy.get('div[class$="popoverFooter"]').contains('button', 'Add').click() - - // Adding a preset calls onPresetChange with the new color prepended to the - // existing colors array. - cy.wrap(onPresetChange).should('have.been.calledOnce') - cy.wrap(onPresetChange) - .its('lastCall.args.0') - .should('have.length', testValue.colors.length + 1) - }) - - it('should allow removing presets', () => { - const onPresetChange = cy.spy() - cy.mount( - - ) - const lastColorIndex = testValue.colors.length - 1 - const expectedColors = testValue.colors.slice(0, -1) - - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(lastColorIndex) - .click() - cy.contains('li', 'Remove').realClick() - cy.wrap(onPresetChange).should('have.been.calledWithMatch', expectedColors) - }) - - it('should allow selecting presets', () => { - const testableIdx = 3 - const onSelect = cy.spy() - cy.mount( - - ) - cy.get('div[role="presentation"][class$="-colorIndicator"]') - .eq(testableIdx) - .click() - cy.contains('li', 'Select').realClick() - cy.wrap(onSelect).should( - 'have.been.calledWith', - testValue.colors[testableIdx] - ) - }) -}) diff --git a/cypress/component/ToggleButton.cy.tsx b/cypress/component/ToggleButton.cy.tsx deleted file mode 100644 index f4efc25cec..0000000000 --- a/cypress/component/ToggleButton.cy.tsx +++ /dev/null @@ -1,68 +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 'cypress-real-events' - -import '../support/component' -import { ToggleButton } from '@instructure/ui/latest' - -describe('', () => { - const icon = ( - - - - ) - const iconSelector = 'svg[data-title="myIcon"]' - - it('should display a tooltip', () => { - cy.mount( - - ) - cy.get('button').find(iconSelector).should('exist') - cy.contains('Tooltip content').should('not.be.visible') - - cy.contains('button', 'This is a screen reader label').realHover().wait(100) - cy.contains('Tooltip content').should('be.visible') - }) - - it('should display a tooltip without hover/focus when isShowingTooltip is true', () => { - cy.mount( - - ) - cy.contains('button', 'This is a screen reader label') - cy.get('button').find(iconSelector).should('exist') - cy.contains('Tooltip content').should('be.visible') - }) -}) diff --git a/packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx b/packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx index b0d5af63fe..b6ac99acb0 100644 --- a/packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx +++ b/packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx @@ -22,13 +22,12 @@ * SOFTWARE. */ -import { fireEvent, render, screen } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { IconUserLine } from '@instructure/ui-icons' import { Billboard } from '@instructure/ui-billboard/latest' import { runAxeCheck } from '@instructure/ui-axe-check' -import '@testing-library/jest-dom' const TEST_HEADING = 'test-heading' const TEST_MESSAGE = 'test-message' @@ -54,14 +53,14 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should render', () => { - const { container } = render() + it('should render', async () => { + const { container } = await render() expect(container.firstChild).toBeInTheDocument() }) it('should be accessible', async () => { - const { container } = render( + const { container } = await render( ', () => { expect(axeCheck).toBe(true) }) - it('should render a heading with the correct tag', () => { - render() - const heading = screen.getByText(TEST_HEADING) + it('should render a heading with the correct tag', async () => { + await render() + const heading = page.getByText(TEST_HEADING).element() expect(heading).toBeInTheDocument() expect(heading.tagName).toBe('H2') }) - it('renders as a link if it has an href prop', () => { - render() + it('renders as a link if it has an href prop', async () => { + await render() - const link = screen.getByRole('link') + const link = page.getByRole('link').element() expect(link).toBeInTheDocument() expect(link).toHaveAttribute('href', TEST_LINK) }) - it('renders as a button and responds to onClick event', () => { + it('renders as a button and responds to onClick event', async () => { const onClick = vi.fn() - render() - const button = screen.getByRole('button') + await render() - fireEvent.click(button) + await userEvent.click(page.getByRole('button')) expect(onClick).toHaveBeenCalledTimes(1) }) @@ -105,18 +103,18 @@ describe('', () => { it('should render message when passed a node', async () => { const messageNode = {TEST_MESSAGE} - render() - const messageElement = screen.getByText(TEST_MESSAGE) + await render() + const messageElement = page.getByText(TEST_MESSAGE).element() expect(messageElement).toBeInTheDocument() expect(messageElement.tagName).toBe('SPAN') }) - it('should render message passed a function', () => { + it('should render message passed a function', async () => { const messageNode = {TEST_MESSAGE} - render( messageNode} />) - const messageElement = screen.getByText(TEST_MESSAGE) + await render( messageNode} />) + const messageElement = page.getByText(TEST_MESSAGE).element() expect(messageElement).toBeInTheDocument() expect(messageElement.tagName).toBe('SPAN') @@ -124,25 +122,25 @@ describe('', () => { }) describe('when disabled', () => { - it('should apply aria-disabled to link', () => { - render() - const link = screen.getByRole('link') + it('should apply aria-disabled to link', async () => { + await render() + const link = page.getByRole('link').element() expect(link).toHaveAttribute('aria-disabled', 'true') }) - it('should not be clickable', () => { - render() - const button = screen.getByRole('button') + it('should not be clickable', async () => { + await render() + const button = page.getByRole('button').element() expect(button).toHaveAttribute('aria-disabled', 'true') }) }) describe('when readOnly', () => { - it('should apply aria-disabled', () => { - render() - const link = screen.getByRole('link') + it('should apply aria-disabled', async () => { + await render() + const link = page.getByRole('link').element() expect(link).toHaveAttribute('aria-disabled', 'true') }) @@ -150,27 +148,28 @@ describe('', () => { it('should not be clickable', async () => { const onClick = vi.fn() - render() - const button = screen.getByRole('button') + await render() - await userEvent.click(button) + // `force` because Playwright treats aria-disabled elements as disabled + // and would otherwise refuse to click + await userEvent.click(page.getByRole('button'), { force: true }) expect(onClick).not.toHaveBeenCalled() }) }) describe('when passing down props to View', () => { - it('should support an elementRef prop', () => { + it('should support an elementRef prop', async () => { const elementRef = vi.fn() - render() - const link = screen.getByRole('link') + await render() + const link = page.getByRole('link').element() expect(elementRef).toHaveBeenCalledWith(link) }) - it('should support an `as` prop', () => { - const { container } = render() + it('should support an `as` prop', async () => { + const { container } = await render() const billboardAsEm = container.querySelector('em') expect(billboardAsEm).toBeInTheDocument() diff --git a/packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx index d4e89a563a..dc1bae6526 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx @@ -22,10 +22,10 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import { vi } from 'vitest' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import type { MockInstance } from 'vitest' -import '@testing-library/jest-dom' import { runAxeCheck } from '@instructure/ui-axe-check' import { Breadcrumb } from '@instructure/ui-breadcrumb/latest' @@ -54,7 +54,7 @@ describe('', () => { }) it('should be accessible', async () => { - const { container } = render( + const { container } = await render( {TEST_TEXT_01} {TEST_TEXT_02} @@ -65,21 +65,21 @@ describe('', () => { expect(axeCheck).toBe(true) }) - it('should render the label as an aria-label attribute', () => { - render( + it('should render the label as an aria-label attribute', async () => { + await render( {TEST_TEXT_01} ) - const label = screen.getByLabelText(TEST_LABEL) + const label = page.getByLabelText(TEST_LABEL).element() expect(label).toBeInTheDocument() expect(label).toHaveAttribute('aria-label', TEST_LABEL) }) - it('should render an icon as a separator', () => { - const { container } = render( + it('should render an icon as a separator', async () => { + const { container } = await render( {TEST_TEXT_01} {TEST_TEXT_02} @@ -91,8 +91,8 @@ describe('', () => { expect(icon).toHaveAttribute('aria-hidden', 'true') }) - it('should add aria-current="page" to the last element by default', () => { - const { container } = render( + it('should add aria-current="page" to the last element by default', async () => { + const { container } = await render( {TEST_TEXT_01} {TEST_TEXT_02} @@ -106,8 +106,8 @@ describe('', () => { expect(lastLink).toHaveAttribute('aria-current', 'page') }) - it('should add aria-current="page" to the element if isCurrent is true', () => { - const { container } = render( + it('should add aria-current="page" to the element if isCurrent is true', async () => { + const { container } = await render( {TEST_TEXT_01} @@ -144,8 +144,8 @@ describe('', () => { // ) // }) - it('should not add aria-current="page" to the last element if it set to false', () => { - const { container } = render( + it('should not add aria-current="page" to the last element if it set to false', async () => { + const { container } = await render( {TEST_TEXT_01} {TEST_TEXT_02} diff --git a/packages/ui-breadcrumb/src/Breadcrumb/__tests__/BreadcrumbLink.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/__tests__/BreadcrumbLink.test.tsx index 8ca1394242..a02d57af54 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/__tests__/BreadcrumbLink.test.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/__tests__/BreadcrumbLink.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { fireEvent, render, screen } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { BreadcrumbLink } from '@instructure/ui-breadcrumb/latest' @@ -52,40 +52,44 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should render an anchor tag when given a href prop', () => { - render({TEST_TEXT_01}) - const anchor = screen.getByRole('link') + it('should render an anchor tag when given a href prop', async () => { + await render( + {TEST_TEXT_01} + ) + const anchor = page.getByRole('link').element() expect(anchor).toHaveAttribute('href', TEST_LINK) }) - it('should render as a button and respond to onClick event', () => { + it('should render as a button and respond to onClick event', async () => { const onClick = vi.fn() - render({TEST_TEXT_01}) - const button = screen.getByRole('button') + await render( + {TEST_TEXT_01} + ) + const button = page.getByRole('button') - fireEvent.click(button) + await userEvent.click(button) expect(onClick).toHaveBeenCalledTimes(1) }) - it('should respond to mouseEnter event when provided with onMouseEnter prop', () => { + it('should respond to mouseEnter event when provided with onMouseEnter prop', async () => { const onMouseEnter = vi.fn() - render( + await render( {TEST_TEXT_01} ) - const link = screen.getByRole('link') - fireEvent.mouseEnter(link) + const link = page.getByRole('link') + await userEvent.hover(link) expect(onMouseEnter).toHaveBeenCalledTimes(1) }) - it('should allow to prop to pass through', () => { - const { container } = render( + it('should allow to prop to pass through', async () => { + const { container } = await render( {TEST_TEXT_01} ) const link = container.querySelector('a') @@ -94,8 +98,8 @@ describe('', () => { expect(link).toHaveAttribute('to', TEST_TO) }) - it('should not render a link when not given an href prop', () => { - const { container } = render( + it('should not render a link when not given an href prop', async () => { + const { container } = await render( {TEST_TEXT_01} ) const elementWithHref = container.querySelector('[href]') @@ -108,8 +112,8 @@ describe('', () => { expect(span).toHaveTextContent(TEST_TEXT_01) }) - it('should not render a button when not given an onClick prop', () => { - const { container } = render( + it('should not render a button when not given an onClick prop', async () => { + const { container } = await render( {TEST_TEXT_01} ) const button = container.querySelector('button') @@ -121,7 +125,7 @@ describe('', () => { }) it('should meet a11y standards as a link', async () => { - const { container } = render( + const { container } = await render( {TEST_TEXT_01} ) const axeCheck = await runAxeCheck(container) @@ -130,7 +134,7 @@ describe('', () => { }) it('should meet a11y standards as a span', async () => { - const { container } = render( + const { container } = await render( {TEST_TEXT_01} ) const axeCheck = await runAxeCheck(container) diff --git a/packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx b/packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx index 6a14b42b08..9549470fec 100644 --- a/packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx +++ b/packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx @@ -22,10 +22,10 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import type { MockInstance } from 'vitest' -import userEvent from '@testing-library/user-event' import { BaseButton } from '@instructure/ui-buttons/latest' import { runAxeCheck } from '@instructure/ui-axe-check' @@ -48,8 +48,8 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should render a button and the children as button text', () => { - render(Hello World) + it('should render a button and the children as button text', async () => { + await render(Hello World) const button = document.querySelector('button') @@ -57,8 +57,8 @@ describe('', () => { expect(button).toHaveTextContent('Hello World') }) - it('should not error with a null child', () => { - render(Hello World{null}) + it('should not error with a null child', async () => { + await render(Hello World{null}) const button = document.querySelector('button') @@ -66,25 +66,29 @@ describe('', () => { expect(button).toHaveTextContent('Hello World') }) - it('should render a link styled as a button if href is provided', () => { - render(Hello World) + it('should render a link styled as a button if href is provided', async () => { + await render(Hello World) - const linkButton = screen.getByRole('link', { name: 'Hello World' }) + const linkButton = page.getByRole('link', { name: 'Hello World' }).element() expect(linkButton).toBeInTheDocument() expect(linkButton).toHaveAttribute('href', 'example.html') }) - it('should render as a link when `to` prop is provided', () => { - const { container } = render(Test) + it('should render as a link when `to` prop is provided', async () => { + const { container } = await render( + Test + ) const linkButton = container.querySelector('a') expect(linkButton!.getAttribute('to')).toBe('/example') }) - it('should render designated tag if `as` prop is specified', () => { - const { container } = render(Hello World) + it('should render designated tag if `as` prop is specified', async () => { + const { container } = await render( + Hello World + ) const button = container.querySelector('[type="button"]') @@ -93,10 +97,10 @@ describe('', () => { expect(button!.tagName).toBe('SPAN') }) - it('should set role="button"', () => { + it('should set role="button"', async () => { const onClick = vi.fn() - const { container } = render( + const { container } = await render( Hello World @@ -108,25 +112,25 @@ describe('', () => { expect(button).toHaveAttribute('role', 'button') }) - it('should set tabIndex="0"', () => { + it('should set tabIndex="0"', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('tabIndex', '0') }) - it('should not set tabIndex="0" when the element has it by default', () => { + it('should not set tabIndex="0" when the element has it by default', async () => { const onClick = vi.fn() - render( + await render( <> Hello Button @@ -136,36 +140,36 @@ describe('', () => { ) - const button = screen.getByRole('button', { name: 'Hello Button' }) + const button = page.getByRole('button', { name: 'Hello Button' }).element() expect(button).toBeInTheDocument() expect(button).not.toHaveAttribute('tabIndex') - const link = screen.getByRole('link', { name: 'Hello link' }) + const link = page.getByRole('link', { name: 'Hello link' }).element() expect(link).toBeInTheDocument() expect(link).not.toHaveAttribute('tabIndex') }) it('should pass down the type prop to the button element', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'submit') }) - it('should pass down an icon via the icon property', () => { + it('should pass down an icon via the icon property', async () => { const SomeIcon = () => ( ) - render(Hello World) + await render(Hello World) const icon = document.querySelector('svg') @@ -173,8 +177,8 @@ describe('', () => { }) it('focuses with the focus helper', async () => { - render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + await render(Hello World) + const button = page.getByRole('button', { name: 'Hello World' }).element() button.focus() @@ -183,20 +187,20 @@ describe('', () => { it('should provide an elementRef prop', async () => { const elementRef = vi.fn() - render(Hello World) + await render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() expect(elementRef).toHaveBeenCalledWith(button) }) - it('should not be underlined when disabled with a href', () => { - render( + it('should not be underlined when disabled with a href', async () => { + await render( Hello World ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() const styles = window.getComputedStyle(button) expect(styles.textDecoration).toContain('none') @@ -205,13 +209,13 @@ describe('', () => { describe('onClick', () => { it('should call onClick when clicked', async () => { const onClick = vi.fn() - render(Hello World) + await render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) @@ -219,16 +223,16 @@ describe('', () => { it('should not call onClick when interaction is "disabled"', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -236,16 +240,16 @@ describe('', () => { it('should not call onClick when disabled is set"', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -253,16 +257,16 @@ describe('', () => { it('should not call onClick when interaction is "readonly"', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -270,16 +274,16 @@ describe('', () => { it('should not call onClick when readOnly is set', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -287,13 +291,13 @@ describe('', () => { it('should not call onClick when button is disabled and an href prop is provided', async () => { const onClick = vi.fn() - render(Hello World) + await render(Hello World) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -301,17 +305,17 @@ describe('', () => { it('should not call onClick when interaction is "readonly" and an href prop is provided', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -319,17 +323,17 @@ describe('', () => { it('should call onClick when space key is pressed if href is provided', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() - await userEvent.type(button, '{space}') + await userEvent.type(button, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() }) }) @@ -337,17 +341,17 @@ describe('', () => { it('should call onClick when enter key is pressed when not a button or link', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.type(button, '{enter}') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() }) }) @@ -355,17 +359,17 @@ describe('', () => { it('should not call onClick when interaction is "disabled" and space key is pressed', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() - await userEvent.type(button, '{space}') + await userEvent.type(button, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -373,16 +377,16 @@ describe('', () => { it('should not call onClick when interaction is "readonly" and space key is pressed', async () => { const onClick = vi.fn() - render( + await render( Hello World ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() - await userEvent.type(button, '{space}') + await userEvent.type(button, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -390,18 +394,18 @@ describe('', () => { describe('when passing down props to View', () => { it("passes cursor='pointer' to View by default", async () => { - render(Hello World) + await render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() const style = window.getComputedStyle(button) expect(style.cursor).toBe('pointer') }) it("passes cursor='not-allowed' to View when disabled", async () => { - render(Hello World) + await render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() const style = window.getComputedStyle(button) expect(style.cursor).toBe('not-allowed') @@ -412,13 +416,13 @@ describe('', () => { it('should meet standards when onClick is given', async () => { const onClick = vi.fn() - render(Hello World) + await render(Hello World) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(async () => { + await vi.waitFor(async () => { const axeCheck = await runAxeCheck(button) expect(axeCheck).toBe(true) @@ -426,44 +430,52 @@ describe('', () => { }) describe('when disabled', () => { - it('sets the disabled attribute so that the button is not in tab order', () => { - render(Hello World) + it('sets the disabled attribute so that the button is not in tab order', async () => { + await render( + Hello World + ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page + .getByRole('button', { name: 'Hello World' }) + .element() expect(button).toHaveAttribute('disabled') }) }) describe('when readonly', () => { it('sets the disabled attribute so that the button is not in tab order', async () => { - render(Hello World) + await render( + Hello World + ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page + .getByRole('button', { name: 'Hello World' }) + .element() expect(button).toHaveAttribute('disabled') }) }) }) describe('href safety', () => { - it('strips javascript: schemes from href', () => { - render(Hi) - const el = screen.getByText('Hi').closest('a, button') + it('strips javascript: schemes from href', async () => { + await render(Hi) + const el = page.getByText('Hi').element().closest('a, button') expect(el).not.toHaveAttribute('href') }) - it('preserves safe https hrefs', () => { - render(Hi) - const link = screen.getByRole('link') + it('preserves safe https hrefs', async () => { + await render(Hi) + const link = page.getByRole('link').element() expect(link).toHaveAttribute('href', 'https://example.com') }) - it('defaults rel for target=_blank', () => { - render( + it('defaults rel for target=_blank', async () => { + await render( Hi ) - const link = screen.getByRole('link') + const link = page.getByRole('link').element() expect(link).toHaveAttribute('rel', 'noopener noreferrer') }) }) diff --git a/packages/ui-buttons/src/Button/__tests__/Button.test.tsx b/packages/ui-buttons/src/Button/__tests__/Button.test.tsx index a30504f273..0e9dbc13ef 100644 --- a/packages/ui-buttons/src/Button/__tests__/Button.test.tsx +++ b/packages/ui-buttons/src/Button/__tests__/Button.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { BaseButton, Button } from '@instructure/ui-buttons/latest' @@ -61,10 +60,10 @@ describe(') + await render() const button = document.querySelector('button') @@ -73,19 +72,19 @@ describe(') + await render() - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'button') expect(button).toHaveTextContent('Hello World') }) - it('should provide a focused getter', () => { + it('should provide a focused getter', async () => { let componentRef: BaseButton | undefined - render( + await render( ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() button.focus() expect(componentRef?.focused).toBe(true) }) - it('should provide a focus function', () => { + it('should provide a focus function', async () => { let componentRef: BaseButton | undefined - render( + await render( ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() componentRef?.focus() expect(document.activeElement).toBe(button) }) - it('should pass the type attribute', () => { - render() + it('should pass the type attribute', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'submit') }) - it('should pass the `elementRef` prop', () => { + it('should pass the `elementRef` prop', async () => { const elementRef = vi.fn() - render() + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(elementRef).toHaveBeenCalledWith(button) }) - it('should pass the `as` prop', () => { - const { container } = render() + it('should pass the `as` prop', async () => { + const { container } = await render() const button = container.querySelector('[type="button"]') @@ -150,52 +149,52 @@ describe(') + it('should set the disabled attribute when `interaction` is set to disabled', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `disabled` is set', () => { - render() + it('should set the disabled attribute when `disabled` is set', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `interaction` is set to readonly', () => { - render() + it('should set the disabled attribute when `interaction` is set to readonly', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `readOnly` is set', () => { - render() + it('should set the disabled attribute when `readOnly` is set', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should pass the `href` prop', () => { - render() + it('should pass the `href` prop', async () => { + await render() - const linkButton = screen.getByRole('link', { name: 'Hello' }) + const linkButton = page.getByRole('link', { name: 'Hello' }).element() expect(linkButton).toBeInTheDocument() expect(linkButton).toHaveAttribute('href', '#') }) - it('should pass the `renderIcon` prop', () => { - render() + it('should pass the `renderIcon` prop', async () => { + await render() const svgIcon = document.querySelector(iconSelector) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toBeInTheDocument() expect(svgIcon).toBeInTheDocument() @@ -203,45 +202,45 @@ describe(') + await render() - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) - it('should render the children as button text', () => { - render() + it('should render the children as button text', async () => { + await render() - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(button).toHaveTextContent('Hello World') }) - it('should not error with a null child', () => { - render() + it('should not error with a null child', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() expect(button).toBeInTheDocument() }) - it('should render a link styled as a button if href is provided', () => { - render() + it('should render a link styled as a button if href is provided', async () => { + await render() - const button = screen.getAllByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).elements() expect(button).toHaveLength(1) expect(button[0]).toHaveAttribute('href', 'example.html') }) - it('should render as a link when `to` prop is provided', () => { - const { container } = render() + it('should render as a link when `to` prop is provided', async () => { + const { container } = await render() const linkButton = container.querySelector('a') @@ -249,8 +248,8 @@ describe(') + it('should render designated tag if `as` prop is specified', async () => { + const { container } = await render() const button = container.querySelector('[type="button"]') @@ -259,9 +258,9 @@ describe(' @@ -272,37 +271,37 @@ describe(' ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('tabIndex', '0') }) - it('should pass down the type prop to the button element', () => { + it('should pass down the type prop to the button element', async () => { const onClick = vi.fn() - render( + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'submit') }) - it('focuses with the focus helper', () => { + it('focuses with the focus helper', async () => { const onFocus = vi.fn() - render() + await render() - const button = screen.getByRole('button') + const button = page.getByRole('button').element() button.focus() @@ -313,13 +312,13 @@ describe(') + await render() - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() }) }) @@ -327,16 +326,16 @@ describe(' ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -344,16 +343,16 @@ describe(' ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() - await userEvent.click(button) + await userEvent.click(button, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -361,13 +360,13 @@ describe(') + await render() - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -375,16 +374,16 @@ describe(' ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -392,16 +391,16 @@ describe(' ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() - await userEvent.type(button, '{space}') + await userEvent.type(button, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() }) }) @@ -409,16 +408,16 @@ describe(' ) - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.type(button, '{enter}') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() }) }) @@ -426,16 +425,16 @@ describe(' ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() await userEvent.type(button, '{spaec}') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -443,16 +442,16 @@ describe(' ) - const button = screen.getByRole('link', { name: 'Hello World' }) + const button = page.getByRole('link', { name: 'Hello World' }).element() - await userEvent.type(button, '{space}') + await userEvent.type(button, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() }) }) @@ -461,13 +460,13 @@ describe(') + await render() - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page.getByRole('button', { name: 'Hello World' }).element() await userEvent.click(button) - await waitFor(async () => { + await vi.waitFor(async () => { const axeCheck = await runAxeCheck(button) expect(axeCheck).toBe(true) @@ -475,20 +474,24 @@ describe(') + it('sets the disabled attribute so that the button is not in tab order', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page + .getByRole('button', { name: 'Hello World' }) + .element() expect(button).toHaveAttribute('disabled') }) }) describe('when readOnly', () => { - it('sets the disabled attribute so that the button is not in tab order', () => { - render() + it('sets the disabled attribute so that the button is not in tab order', async () => { + await render() - const button = screen.getByRole('button', { name: 'Hello World' }) + const button = page + .getByRole('button', { name: 'Hello World' }) + .element() expect(button).toHaveAttribute('disabled') }) diff --git a/packages/ui-buttons/src/CloseButton/__tests__/CloseButton.test.tsx b/packages/ui-buttons/src/CloseButton/__tests__/CloseButton.test.tsx index 77bcb9cb5e..ebb4991ec6 100644 --- a/packages/ui-buttons/src/CloseButton/__tests__/CloseButton.test.tsx +++ b/packages/ui-buttons/src/CloseButton/__tests__/CloseButton.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { CloseButton } from '@instructure/ui-buttons/latest' describe('', () => { @@ -42,9 +42,9 @@ describe('', () => { }) it('should render with x icon', async () => { - render() + await render() - const button = screen.getByRole('button') + const button = page.getByRole('button').element() const icon = document.querySelector('svg') @@ -57,13 +57,13 @@ describe('', () => { it('should pass the `onClick` prop', async () => { const onClick = vi.fn() - render() + await render() - const button = screen.getByRole('button') + const button = page.getByRole('button').element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/ui-buttons/src/CondensedButton/__tests__/CondensedButton.test.tsx b/packages/ui-buttons/src/CondensedButton/__tests__/CondensedButton.test.tsx index 245fdf45b0..9a2cc0ae23 100644 --- a/packages/ui-buttons/src/CondensedButton/__tests__/CondensedButton.test.tsx +++ b/packages/ui-buttons/src/CondensedButton/__tests__/CondensedButton.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { CondensedButton } from '@instructure/ui-buttons/latest' describe('', () => { @@ -40,10 +40,10 @@ describe('', () => { ) const iconSelector = 'svg[data-title="myIcon"]' - it('should render children', () => { + it('should render children', async () => { const children = 'Hello world' - render({children}) + await render({children}) const button = document.querySelector('button') @@ -51,10 +51,10 @@ describe('', () => { expect(button).toHaveTextContent(children) }) - it('should provide a focused getter', () => { + it('should provide a focused getter', async () => { let componentRef: CondensedButton | undefined - render( + await render( { componentRef = component @@ -63,17 +63,17 @@ describe('', () => { Hello ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() button.focus() expect(componentRef?.focused).toBe(true) }) - it('should provide a focus function', () => { + it('should provide a focus function', async () => { let componentRef: CondensedButton | undefined - render( + await render( { componentRef = component @@ -82,17 +82,17 @@ describe('', () => { Hello ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() componentRef?.focus() expect(document.activeElement).toBe(button) }) - it('should pass the type attribute', () => { - render(Hello) + it('should pass the type attribute', async () => { + await render(Hello) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'submit') @@ -100,14 +100,16 @@ describe('', () => { it('should pass the `elementRef` prop', async () => { const elementRef = vi.fn() - render(Hello) - const button = screen.getByRole('button', { name: 'Hello' }) + await render( + Hello + ) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(elementRef).toHaveBeenCalledWith(button) }) - it('should pass the `as` prop', () => { - const { container } = render( + it('should pass the `as` prop', async () => { + const { container } = await render( Hello ) @@ -118,52 +120,56 @@ describe('', () => { expect(button!.tagName).toBe('LI') }) - it('should set the disabled attribute when `interaction` is set to disabled', () => { - render(Hello) + it('should set the disabled attribute when `interaction` is set to disabled', async () => { + await render( + Hello + ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `disabled` is set', () => { - render(Hello) + it('should set the disabled attribute when `disabled` is set', async () => { + await render(Hello) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `interaction` is set to readonly', () => { - render(Hello) + it('should set the disabled attribute when `interaction` is set to readonly', async () => { + await render( + Hello + ) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `readOnly` is set', () => { - render(Hello) + it('should set the disabled attribute when `readOnly` is set', async () => { + await render(Hello) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toHaveAttribute('disabled') }) - it('should pass the `href` prop', () => { - render(Hello) + it('should pass the `href` prop', async () => { + await render(Hello) - const linkButton = screen.getByRole('link', { name: 'Hello' }) + const linkButton = page.getByRole('link', { name: 'Hello' }).element() expect(linkButton).toBeInTheDocument() expect(linkButton).toHaveAttribute('href', '#') }) it('should pass the `renderIcon` prop', async () => { - render(Hello) + await render(Hello) const svgIcon = document.querySelector(iconSelector) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() expect(button).toBeInTheDocument() expect(svgIcon).toBeInTheDocument() @@ -171,13 +177,13 @@ describe('', () => { it('should pass the `onClick` prop', async () => { const onClick = vi.fn() - render(Hello) + await render(Hello) - const button = screen.getByRole('button', { name: 'Hello' }) + const button = page.getByRole('button', { name: 'Hello' }).element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/ui-buttons/src/IconButton/__tests__/IconButton.test.tsx b/packages/ui-buttons/src/IconButton/__tests__/IconButton.test.tsx index 067b915f66..059d1c2dbb 100644 --- a/packages/ui-buttons/src/IconButton/__tests__/IconButton.test.tsx +++ b/packages/ui-buttons/src/IconButton/__tests__/IconButton.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { IconButton } from '@instructure/ui-buttons/latest' describe('', () => { @@ -40,30 +40,34 @@ describe('', () => { ) const iconSelector = 'svg[data-title="myIcon"]' - it('should render an icon when provided as the `children` prop', () => { - render({icon}) + it('should render an icon when provided as the `children` prop', async () => { + await render( + {icon} + ) const svgIcon = document.querySelector(iconSelector) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(svgIcon).toBeInTheDocument() expect(button).toHaveTextContent('some action') }) - it('should render an icon when provided as the `renderIcon` prop', () => { - render() + it('should render an icon when provided as the `renderIcon` prop', async () => { + await render( + + ) const svgIcon = document.querySelector(iconSelector) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(svgIcon).toBeInTheDocument() }) - it('should provide a focused getter', () => { + it('should provide a focused getter', async () => { let componentRef: IconButton | undefined - render( + await render( ', () => { }} /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() button.focus() expect(componentRef?.focused).toBe(true) }) - it('should provide a focus function', () => { + it('should provide a focus function', async () => { let componentRef: IconButton | undefined - render( + await render( ', () => { }} /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() componentRef?.focus() expect(document.activeElement).toBe(button) }) - it('should pass the `href` prop', () => { - render( + it('should pass the `href` prop', async () => { + await render( ) - const linkButton = screen.getByRole('link') + const linkButton = page.getByRole('link').element() expect(linkButton).toBeInTheDocument() expect(linkButton).toHaveAttribute('href', '#') }) - it('should pass the `type` prop', () => { - render( + it('should pass the `type` prop', async () => { + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toBeInTheDocument() expect(button).toHaveAttribute('type', 'reset') }) - it('should pass the `elementRef` prop', () => { + it('should pass the `elementRef` prop', async () => { const elementRef = vi.fn() - render( + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(elementRef).toHaveBeenCalledWith(button) }) - it('should pass the `as` prop', () => { - const { container } = render( + it('should pass the `as` prop', async () => { + const { container } = await render( ) const button = container.querySelector('[type="button"]') @@ -147,46 +151,46 @@ describe('', () => { expect(button!.tagName).toBe('LI') }) - it('should set the disabled attribute when `interaction` is set to disabled', () => { - render( + it('should set the disabled attribute when `interaction` is set to disabled', async () => { + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `disabled` is set', () => { - render( + it('should set the disabled attribute when `disabled` is set', async () => { + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `interaction` is set to readonly', () => { - render( + it('should set the disabled attribute when `interaction` is set to readonly', async () => { + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `readOnly` is set', () => { - render( + it('should set the disabled attribute when `readOnly` is set', async () => { + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) @@ -194,18 +198,18 @@ describe('', () => { it('should pass the `onClick` prop', async () => { const onClick = vi.fn() - render( + await render( ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx index f2acf644cc..84e483ce5b 100644 --- a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx +++ b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { ToggleButton } from '@instructure/ui-buttons/latest' describe('', () => { @@ -35,8 +35,8 @@ describe('', () => { ) const iconSelector = 'svg[data-title="myIcon"]' - it('should render', () => { - render( + it('should render', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() const svgIcon = document.querySelector(iconSelector) - const tooltip = screen.getByRole('tooltip') + // queried directly: the tooltip is hidden, so it is not in the a11y tree + const tooltip = document.querySelector('[role="tooltip"]') expect(button).toBeInTheDocument() expect(button).toHaveTextContent('This is a screen reader label') @@ -55,8 +56,8 @@ describe('', () => { expect(tooltip).toHaveTextContent('This is tooltip content') }) - it('should set `aria-pressed` to `true` if `status` is `pressed`', () => { - render( + it('should set `aria-pressed` to `true` if `status` is `pressed`', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('aria-pressed', 'true') }) - it('should set `aria-pressed` to `false` if `status` is `unpressed`', () => { - render( + it('should set `aria-pressed` to `false` if `status` is `unpressed`', async () => { + await render( ', () => { status="unpressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('aria-pressed', 'false') }) - it('should render an icon', () => { - render( + it('should render an icon', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() const svgIcon = document.querySelector(iconSelector) expect(button).toBeInTheDocument() expect(svgIcon).toBeInTheDocument() }) - it('should pass the `as` prop', () => { - const { container } = render( + it('should pass the `as` prop', async () => { + const { container } = await render( ', () => { expect(button!.tagName).toBe('LI') }) - it('should set the disabled attribute when `interaction` prop is set to disabled', () => { - render( + it('should set the disabled attribute when `interaction` prop is set to disabled', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `disabled` prop is set', () => { - render( + it('should set the disabled attribute when `disabled` prop is set', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `interaction` prop is set to readonly', () => { - render( + it('should set the disabled attribute when `interaction` prop is set to readonly', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) - it('should set the disabled attribute when `readOnly` prop is set', () => { - render( + it('should set the disabled attribute when `readOnly` prop is set', async () => { + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(button).toHaveAttribute('disabled') }) @@ -178,7 +179,7 @@ describe('', () => { it('should pass the `onClick` prop', async () => { const onClick = vi.fn() - render( + await render( ', () => { status="pressed" /> ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() await userEvent.click(button) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalledTimes(1) }) }) + + it('should display a tooltip on hover', async () => { + await render( + + ) + const button = page.getByRole('button', { + name: 'This is a screen reader label' + }) + + expect(button.element().querySelector(iconSelector)).toBeInTheDocument() + // an earlier test may have left the pointer over the same spot + await userEvent.unhover(button) + // while hidden the tooltip is not in the a11y tree, so it has no role + await expect.element(page.getByRole('tooltip')).not.toBeInTheDocument() + + await userEvent.hover(button) + + const tooltip = page.getByRole('tooltip') + await expect.element(tooltip).toBeVisible() + await expect.element(tooltip).toHaveTextContent('Tooltip content') + }) + + it('should display a tooltip without hover/focus when isShowingTooltip is true', async () => { + await render( + + ) + const button = page.getByRole('button', { + name: 'This is a screen reader label' + }) + + expect(button.element().querySelector(iconSelector)).toBeInTheDocument() + + const tooltip = page.getByRole('tooltip') + await expect.element(tooltip).toBeVisible() + await expect.element(tooltip).toHaveTextContent('Tooltip content') + }) }) diff --git a/packages/ui-byline/src/Byline/__tests__/Byline.test.tsx b/packages/ui-byline/src/Byline/__tests__/Byline.test.tsx index 05f3a1a04e..c7f8b566be 100644 --- a/packages/ui-byline/src/Byline/__tests__/Byline.test.tsx +++ b/packages/ui-byline/src/Byline/__tests__/Byline.test.tsx @@ -23,13 +23,22 @@ */ import { ComponentType } from 'react' -import { render, screen } from '@testing-library/react' -import { vi } from 'vitest' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, + vi +} from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { Byline } from '@instructure/ui-byline/latest' import type { BylineProps } from '@instructure/ui-byline/latest' import { View } from '@instructure/ui-view/latest' -import '@testing-library/jest-dom' const TEST_TITLE = 'Test-title' const TEST_DESCRIPTION = 'Test-description' @@ -93,13 +102,13 @@ describe('', () => { View.omitViewProps = originalOmitViewProps }) - it('should render', () => { - const { container } = renderByline() + it('should render', async () => { + const { container } = await renderByline() expect(container.firstChild).toBeInTheDocument() }) - it('should pass down div and its contents via the description property', () => { + it('should pass down div and its contents via the description property', async () => { const descriptionElement = (

@@ -108,9 +117,9 @@ describe('', () => {

{TEST_PARAGRAPH}

) - renderByline({ description: descriptionElement }) - const clickableHeading = screen.getByText(TEST_HEADING) - const descriptionText = screen.getByText(TEST_PARAGRAPH) + await renderByline({ description: descriptionElement }) + const clickableHeading = page.getByText(TEST_HEADING).element() + const descriptionText = page.getByText(TEST_PARAGRAPH).element() expect(clickableHeading.tagName).toBe('A') expect(clickableHeading).toHaveAttribute('href', TEST_LINK) @@ -121,7 +130,7 @@ describe('', () => { }) it('should meet a11y standards', async () => { - const { container } = renderByline() + const { container } = await renderByline() const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) @@ -138,16 +147,16 @@ describe('', () => { }) testPropsToAllow.forEach((prop) => { - it(`should allow the '${prop}' prop`, () => { - renderByline({ [prop]: customAllowedProps[prop] }) + it(`should allow the '${prop}' prop`, async () => { + await renderByline({ [prop]: customAllowedProps[prop] }) expect(consoleErrorMock).not.toHaveBeenCalled() }) }) testPropsToDisallow.forEach((prop) => { - it(`should NOT allow the '${prop}' prop`, () => { - renderByline({ [prop]: 'foo' }) + it(`should NOT allow the '${prop}' prop`, async () => { + await renderByline({ [prop]: 'foo' }) const expectedWarningMessage = `Warning: [Byline] prop '${prop}' is not allowed.` const warningMessage = consoleErrorMock.mock.calls[0][0] diff --git a/packages/ui-calendar/src/Calendar/__tests__/Calendar.test.tsx b/packages/ui-calendar/src/Calendar/__tests__/Calendar.test.tsx index d36365b7ee..40eeed65e8 100644 --- a/packages/ui-calendar/src/Calendar/__tests__/Calendar.test.tsx +++ b/packages/ui-calendar/src/Calendar/__tests__/Calendar.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { Calendar } from '@instructure/ui-calendar/latest' import type { CalendarDayProps } from '@instructure/ui-calendar/latest' @@ -72,14 +71,14 @@ describe('', () => { describe('with minimal config', () => { it('should render 44 buttons without config', async () => { - render() + await render() const buttons = document.getElementsByTagName('button') expect(buttons.length).toEqual(44) }) it('should render proper week names by default', async () => { - const { container } = render() + const { container } = await render() const thead = container.querySelector('thead') expect(thead).toHaveTextContent( @@ -88,7 +87,7 @@ describe('', () => { }) it('should render proper week names if locale is set', async () => { - const { container } = render( + const { container } = await render( ) const thead = container.querySelector('thead') @@ -99,7 +98,7 @@ describe('', () => { }) it('should disable days properly', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should indicate selected day', async () => { - render( + await render( ', () => { /> ) - const selectedDay = screen.queryByText('22 December 2023, Selected') + const selectedDay = page.getByText('22 December 2023, Selected').query() ?.parentElement?.parentElement expect(selectedDay).toBeDefined() - expect(window.getComputedStyle(selectedDay!).background).toBe( + expect(window.getComputedStyle(selectedDay!).backgroundColor).toBe( 'rgb(3, 137, 61)' ) }) }) it('should render children', async () => { - const { container } = render( + const { container } = await render( {generateDays()} @@ -154,7 +153,7 @@ describe('', () => { it(`should warn if the correct number of children are not provided`, async () => { const count = Calendar.DAY_COUNT - 1 - render( + await render( {generateDays(count)} @@ -169,7 +168,7 @@ describe('', () => { }) it('should render weekday labels', async () => { - const { container, rerender } = render( + const { container, rerender } = await render( {generateDays()} @@ -193,7 +192,7 @@ describe('', () => { ] // Set prop: renderWeekdayLabels - rerender( + await rerender( ', () => { }) it('should warn if 7 weekday labels are not provided', async () => { - render( + await render( {generateDays()} @@ -227,7 +226,7 @@ describe('', () => { }) it('should format the weekday labels and days correctly', async () => { - const { container } = render( + const { container } = await render( {generateDays()} @@ -256,7 +255,7 @@ describe('', () => { ) - const { rerender } = render( + const { rerender } = await render( ', () => { {generateDays()} ) - const month = screen.getByText('March') - const year = screen.getByText('2019') + const month = page.getByText('March', { exact: true }).element() + const year = page.getByText('2019', { exact: true }).element() expect(month).toBeInTheDocument() expect(year).toBeInTheDocument() // Set prop: renderNavigationLabel - rerender( + await rerender( navLabel} @@ -281,30 +280,34 @@ describe('', () => { {generateDays()} ) - const updatedMonth = screen.getByText('March') - const updatedYear = screen.getByText('2019') + const updatedMonth = page.getByText('March', { exact: true }).element() + const updatedYear = page.getByText('2019', { exact: true }).element() expect(updatedMonth).toBeInTheDocument() expect(updatedYear).toBeInTheDocument() }) it('should render next and prev buttons', async () => { - const { rerender } = render( + const { rerender } = await render( {generateDays()} ) - const defaultPrevButton = screen.getByRole('button', { - name: /^Previous month/ - }) - const defaultNextButton = screen.getByRole('button', { - name: /^Next month/ - }) + const defaultPrevButton = page + .getByRole('button', { + name: /^Previous month/ + }) + .element() + const defaultNextButton = page + .getByRole('button', { + name: /^Next month/ + }) + .element() expect(defaultPrevButton).toBeInTheDocument() expect(defaultNextButton).toBeInTheDocument() - rerender( + await rerender( test-prev} @@ -314,13 +317,13 @@ describe('', () => { {generateDays()} ) - const customPrevButton = screen.getByText('test-prev') - const customNextButton = screen.getByText('test-next') + const customPrevButton = page.getByText('test-prev').element() + const customNextButton = page.getByText('test-next').element() expect(customPrevButton).toBeInTheDocument() expect(customNextButton).toBeInTheDocument() - rerender( + await rerender( } @@ -330,8 +333,8 @@ describe('', () => { {generateDays()} ) - const funcPrevButton = screen.getByText('func-test-prev') - const funcNextButton = screen.getByText('func-test-next') + const funcPrevButton = page.getByText('func-test-prev').element() + const funcNextButton = page.getByText('func-test-next').element() expect(funcPrevButton).toBeInTheDocument() expect(funcNextButton).toBeInTheDocument() @@ -341,7 +344,7 @@ describe('', () => { const onRequestRenderPrevMonth = vi.fn() const onRequestRenderNextMonth = vi.fn() - render( + await render( prev month} @@ -354,8 +357,8 @@ describe('', () => { ) - const prevButton = screen.getByText('prev month') - const nextButton = screen.getByText('next month') + const prevButton = page.getByText('prev month').element() + const nextButton = page.getByText('next month').element() expect(onRequestRenderPrevMonth).not.toHaveBeenCalled() expect(onRequestRenderNextMonth).not.toHaveBeenCalled() @@ -363,7 +366,7 @@ describe('', () => { await userEvent.click(prevButton) await userEvent.click(nextButton) - await waitFor(() => { + await vi.waitFor(() => { expect(onRequestRenderPrevMonth).toHaveBeenCalledTimes(1) expect(onRequestRenderNextMonth).toHaveBeenCalledTimes(1) }) @@ -371,7 +374,7 @@ describe('', () => { describe('when role="listbox"', () => { it('should set role="listbox" on table root and role="presentation" on the correct elements', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it("should link each day with it's weekday header via `aria-describedby`", async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should set role="option" and aria-selected on each day', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should announce selected state via accessible label when selectedLabel is provided', async () => { - const { container } = render( + const { container } = await render( ', () => { describe('when role="table" (default)', () => { it('should set role="button" on each day and not set aria-selected', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should announce selected state via accessible label when selectedLabel is provided', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should render root as designated by the `as` prop', async () => { - render( + await render( ', () => { {generateDays()} ) - const calendar = screen.getByRole('list') + const calendar = page.getByRole('list').element() expect(calendar.tagName).toBe('UL') expect(calendar).toHaveTextContent(weekdayLabels.join('')) }) describe('navigation button targetMonthSrLabel', () => { - it('should include the target month in the default button screen reader labels', () => { - render( + it('should include the target month in the default button screen reader labels', async () => { + await render( ', () => { ) expect( - screen.getByRole('button', { name: 'Previous month, November 2023' }) + page + .getByRole('button', { name: 'Previous month, November 2023' }) + .element() ).toBeInTheDocument() expect( - screen.getByRole('button', { name: 'Next month, January 2024' }) + page.getByRole('button', { name: 'Next month, January 2024' }).element() ).toBeInTheDocument() }) - it('should pass targetMonthSrLabel to function render props', () => { - render( + it('should pass targetMonthSrLabel to function render props', async () => { + await render( ', () => { ) expect( - screen.getByRole('button', { name: 'Go to November 2023' }) + page.getByRole('button', { name: 'Go to November 2023' }).element() ).toBeInTheDocument() expect( - screen.getByRole('button', { name: 'Go to January 2024' }) + page.getByRole('button', { name: 'Go to January 2024' }).element() ).toBeInTheDocument() }) }) diff --git a/packages/ui-calendar/src/Calendar/__tests__/Day.test.tsx b/packages/ui-calendar/src/Calendar/__tests__/Day.test.tsx index 64aebd0e4c..65255779cc 100644 --- a/packages/ui-calendar/src/Calendar/__tests__/Day.test.tsx +++ b/packages/ui-calendar/src/Calendar/__tests__/Day.test.tsx @@ -22,15 +22,14 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { CalendarDay as Day } from '@instructure/ui-calendar/latest' describe('Day', () => { it('should render children', async () => { - const { rerender } = render( + const { rerender } = await render( { 8 ) - const child = screen.getByText('8') + const child = page.getByText('8', { exact: true }).element() expect(child).toBeInTheDocument() - rerender( + await rerender( { 31 ) - const childUpdated = screen.getByText('31') + const childUpdated = page.getByText('31', { exact: true }).element() expect(childUpdated).toBeInTheDocument() }) it('should have an accessible label', async () => { const label = '1 August 2019 Friday' - const { container } = render( + const { container } = await render( 8 @@ -73,7 +72,7 @@ describe('Day', () => { }) it('should set aria-current="date" when `isToday`', async () => { - const { container, rerender } = render( + const { container, rerender } = await render( { expect(today).toHaveAttribute('aria-current', 'date') - rerender( + await rerender( { }) it('should not set aria-selected without a role', async () => { - const { container, rerender } = render( + const { container, rerender } = await render( { expect(day).not.toHaveAttribute('aria-selected') - rerender( + await rerender( { }) it('should set aria-selected="true/false" when `isSelected` and `role` is `option` or `gridcell`', async () => { - const { container, rerender } = render( + const { container, rerender } = await render( { const day = container.querySelector('[class*="-calendarDay"]') expect(day).toHaveAttribute('aria-selected', 'false') - rerender( + await rerender( { const daySelected = container.querySelector('[class*="-calendarDay"]') expect(daySelected).toHaveAttribute('aria-selected', 'true') - rerender( + await rerender( { const dayCell = container.querySelector('[class*="-calendarDay"]') expect(dayCell).toHaveAttribute('aria-selected', 'false') - rerender( + await rerender( { const onClick = vi.fn() const date = '2019-08-02' - const { container } = render( + const { container } = await render( { await userEvent.click(day!) - await waitFor(() => { + await vi.waitFor(() => { const args = onClick.mock.calls[0][1] expect(onClick).toHaveBeenCalledTimes(1) @@ -218,7 +217,7 @@ describe('Day', () => { const onKeyDown = vi.fn() const date = '2019-08-02' - const { container } = render( + const { container } = await render( { await userEvent.type(day, '{enter}') - await waitFor(() => { + await vi.waitFor(() => { const args = onKeyDown.mock.calls[0][1] expect(onKeyDown).toHaveBeenCalledTimes(1) @@ -245,7 +244,7 @@ describe('Day', () => { it('should apply disabled when interaction is `disabled`', async () => { const onClick = vi.fn() - const { container } = render( + const { container } = await render( { ) const day = container.querySelector('[class*="-calendarDay"]')! - await userEvent.click(day) + // `force` because Playwright refuses to click disabled elements + await userEvent.click(day, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() expect(day).toHaveAttribute('disabled') }) @@ -271,7 +271,7 @@ describe('Day', () => { const elementRef = (el: Element | null) => { element = el } - const { container } = render( + const { container } = await render( { describe('element type', () => { it('should render as a span by default', async () => { - const { container } = render( + const { container } = await render( { }) it('should render as a button when onClick is provided', async () => { - const { container } = render( + const { container } = await render( { }) it('default elementTypes should be overwritten when `as` prop is set', async () => { - const { container } = render( + const { container } = await render( ', () => { consoleErrorMock.mockRestore() }) - it('renders an input with type "checkbox"', () => { - renderCheckbox() - const inputElem = screen.getByRole('checkbox') + it('renders an input with type "checkbox"', async () => { + await renderCheckbox() + const inputElem = page.getByRole('checkbox').element() expect(inputElem).toBeInTheDocument() expect(inputElem.tagName).toBe('INPUT') @@ -80,7 +79,7 @@ describe('', () => { }) it('`simple` variant only displays a checkmark when checked', async () => { - const { container } = renderCheckbox({ + const { container } = await renderCheckbox({ variant: 'simple', defaultChecked: false }) @@ -89,17 +88,17 @@ describe('', () => { expect(svgElement).not.toBeInTheDocument() - await userEvent.click(checkboxElement!) - await waitFor(() => { + await userEvent.click(checkboxElement!, { force: true }) + await vi.waitFor(() => { const svgElementAfterClick = container.querySelector('svg') expect(svgElementAfterClick).toBeInTheDocument() }) }) - it('`simple` variant supports indeterminate/mixed state', () => { - renderCheckbox({ variant: 'simple', indeterminate: true }) + it('`simple` variant supports indeterminate/mixed state', async () => { + await renderCheckbox({ variant: 'simple', indeterminate: true }) - const inputElem = screen.getByRole('checkbox') + const inputElem = page.getByRole('checkbox').element() expect(inputElem).toBeInTheDocument() expect(inputElem).toHaveAttribute('aria-checked', 'mixed') @@ -107,8 +106,8 @@ describe('', () => { it('should provide an inputRef prop', async () => { const inputRef = vi.fn() - renderCheckbox({ inputRef }) - const input = screen.getByRole('checkbox') + await renderCheckbox({ inputRef }) + const input = page.getByRole('checkbox').element() expect(inputRef).toHaveBeenCalledWith(input) }) @@ -117,12 +116,12 @@ describe('', () => { it('when clicked, fires onClick and onChange events', async () => { const onClick = vi.fn() const onChange = vi.fn() - renderCheckbox({ onClick, onChange }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onClick, onChange }) + const checkboxElement = page.getByRole('checkbox').element() - await userEvent.click(checkboxElement) + await userEvent.click(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).toHaveBeenCalled() expect(onChange).toHaveBeenCalled() }) @@ -131,12 +130,12 @@ describe('', () => { it('when clicked, does not call onClick or onChange when disabled', async () => { const onClick = vi.fn() const onChange = vi.fn() - renderCheckbox({ onClick, onChange, disabled: true }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onClick, onChange, disabled: true }) + const checkboxElement = page.getByRole('checkbox').element() - fireEvent.click(checkboxElement) + await userEvent.click(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() expect(onChange).not.toHaveBeenCalled() expect(checkboxElement).toBeDisabled() @@ -146,12 +145,12 @@ describe('', () => { it('when clicked, does not call onClick or onChange when readOnly', async () => { const onClick = vi.fn() const onChange = vi.fn() - renderCheckbox({ onClick, onChange, readOnly: true }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onClick, onChange, readOnly: true }) + const checkboxElement = page.getByRole('checkbox').element() - fireEvent.click(checkboxElement) + await userEvent.click(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onClick).not.toHaveBeenCalled() expect(onChange).not.toHaveBeenCalled() expect(checkboxElement).not.toBeDisabled() @@ -159,53 +158,55 @@ describe('', () => { }) it('when focused, readOnly checkbox is focusable', async () => { - renderCheckbox({ readOnly: true }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ readOnly: true }) + const checkboxElement = page.getByRole('checkbox').element() checkboxElement.focus() expect(document.activeElement).toBe(checkboxElement) }) - it('calls onChange when enter key is pressed', async () => { + it('calls onChange when space key is pressed', async () => { const onChange = vi.fn() - renderCheckbox({ onChange }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onChange }) + const checkboxElement = page.getByRole('checkbox').element() - await userEvent.type(checkboxElement, '{enter}') + await userEvent.type(checkboxElement, ' ') - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).toHaveBeenCalled() }) }) it('responds to onBlur event', async () => { const onBlur = vi.fn() - renderCheckbox({ onBlur }) + await renderCheckbox({ onBlur }) await userEvent.tab() await userEvent.tab() - await waitFor(() => { + await vi.waitFor(() => { expect(onBlur).toHaveBeenCalled() }) }) it('responds to onFocus event', async () => { const onFocus = vi.fn() - renderCheckbox({ onFocus }) + await renderCheckbox({ onFocus }) - await userEvent.tab() + const checkboxElement = page.getByRole('checkbox').element() + + ;(checkboxElement as HTMLInputElement).focus() - await waitFor(() => { + await vi.waitFor(() => { expect(onFocus).toHaveBeenCalled() }) }) - it('focuses with the focus helper', () => { + it('focuses with the focus helper', async () => { const checkboxRef = createRef() - render() - const checkboxElement = screen.getByRole('checkbox') + await render() + const checkboxElement = page.getByRole('checkbox').element() expect(checkboxElement).not.toHaveFocus() @@ -216,89 +217,91 @@ describe('', () => { it('calls onMouseOver', async () => { const onMouseOver = vi.fn() - renderCheckbox({ onMouseOver }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onMouseOver }) + const checkboxElement = page.getByRole('checkbox').element() - await userEvent.hover(checkboxElement) + await userEvent.hover(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onMouseOver).toHaveBeenCalled() }) }) it('calls onMouseOut', async () => { const onMouseOut = vi.fn() - renderCheckbox({ onMouseOut }) - const checkboxElement = screen.getByRole('checkbox') + await renderCheckbox({ onMouseOut }) + const checkboxElement = page.getByRole('checkbox').element() - await userEvent.hover(checkboxElement) - await userEvent.unhover(checkboxElement) + await userEvent.hover(checkboxElement, { force: true }) + await userEvent.unhover(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onMouseOut).toHaveBeenCalled() }) }) }) it('reflects checked state changes via data-checked attribute', async () => { - renderCheckbox({ defaultChecked: false }) - const input = screen.getByRole('checkbox') + await renderCheckbox({ defaultChecked: false }) + const input = page.getByRole('checkbox').element() expect(input).toHaveAttribute('data-checked', 'false') - await userEvent.click(input) - await waitFor(() => { + await userEvent.click(input, { force: true }) + await vi.waitFor(() => { expect(input).toHaveAttribute('data-checked', 'true') }) }) - it('sets data-checked to mixed when indeterminate', () => { - renderCheckbox({ indeterminate: true }) - const input = screen.getByRole('checkbox') + it('sets data-checked to mixed when indeterminate', async () => { + await renderCheckbox({ indeterminate: true }) + const input = page.getByRole('checkbox').element() expect(input).toHaveAttribute('data-checked', 'mixed') }) - it('should set aria-invalid when errors prop is set', () => { - renderCheckbox({ + it('should set aria-invalid when errors prop is set', async () => { + await renderCheckbox({ messages: [{ type: 'error', text: 'This field is required' }] }) - const input = screen.getByRole('checkbox') + const input = page.getByRole('checkbox').element() expect(input).toHaveAttribute('aria-invalid', 'true') }) - it('should not set aria-invalid when there are no error messages', () => { - renderCheckbox({ messages: [{ type: 'hint', text: 'This is a hint' }] }) - const input = screen.getByRole('checkbox') + it('should not set aria-invalid when there are no error messages', async () => { + await renderCheckbox({ + messages: [{ type: 'hint', text: 'This is a hint' }] + }) + const input = page.getByRole('checkbox').element() expect(input).not.toHaveAttribute('aria-invalid') }) describe('for a11y', () => { it('`simple` variant should meet standards', async () => { - const { container } = renderCheckbox({ variant: 'simple' }) + const { container } = await renderCheckbox({ variant: 'simple' }) const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) }) it('`toggle` variant should meet standards', async () => { - const { container } = renderCheckbox({ variant: 'toggle' }) + const { container } = await renderCheckbox({ variant: 'toggle' }) const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) }) it('associates messages with the checkbox as its description, not its accessible name', async () => { - render( + await render( ) - const input = screen.getByRole('checkbox') + const input = page.getByRole('checkbox').element() const describedById = input.getAttribute('aria-describedby') expect(describedById).toBeTruthy() @@ -314,8 +317,8 @@ describe('', () => { }) it('does not set aria-labelledby when there are no messages', async () => { - render() - const input = screen.getByRole('checkbox') + await render() + const input = page.getByRole('checkbox').element() expect(input).not.toHaveAttribute('aria-labelledby') }) diff --git a/packages/ui-checkbox/src/Checkbox/__tests__/CheckboxFacade.test.tsx b/packages/ui-checkbox/src/Checkbox/__tests__/CheckboxFacade.test.tsx index 290b0dd607..ece61e6a65 100644 --- a/packages/ui-checkbox/src/Checkbox/__tests__/CheckboxFacade.test.tsx +++ b/packages/ui-checkbox/src/Checkbox/__tests__/CheckboxFacade.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { CheckboxFacade } from '@instructure/ui-checkbox/latest' @@ -50,15 +50,17 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should render', () => { - render({TEST_TEXT}) - const facade = screen.getByText(TEST_TEXT) + it('should render', async () => { + await render({TEST_TEXT}) + const facade = page.getByText(TEST_TEXT).element() expect(facade).toBeInTheDocument() }) it('should meet a11y standards', async () => { - const { container } = render({TEST_TEXT}) + const { container } = await render( + {TEST_TEXT} + ) const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) diff --git a/packages/ui-checkbox/src/Checkbox/__tests__/ToggleFacade.test.tsx b/packages/ui-checkbox/src/Checkbox/__tests__/ToggleFacade.test.tsx index bca71237ff..5f24cd6fea 100644 --- a/packages/ui-checkbox/src/Checkbox/__tests__/ToggleFacade.test.tsx +++ b/packages/ui-checkbox/src/Checkbox/__tests__/ToggleFacade.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { ToggleFacade } from '@instructure/ui-checkbox/latest' @@ -50,15 +50,15 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should render', () => { - render({TEST_TEXT}) - const facade = screen.getByText(TEST_TEXT) + it('should render', async () => { + await render({TEST_TEXT}) + const facade = page.getByText(TEST_TEXT).element() expect(facade).toBeInTheDocument() }) it('should meet a11y standards', async () => { - const { container } = render({TEST_TEXT}) + const { container } = await render({TEST_TEXT}) const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) diff --git a/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx b/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx index e3957fca36..5e65888aac 100644 --- a/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx +++ b/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { Checkbox, CheckboxGroup } from '@instructure/ui-checkbox/latest' @@ -73,20 +72,20 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('adds the name props to all Checkbox types', () => { - renderCheckboxGroup({ name: TEST_NAME }) - const checkboxes = screen.getAllByRole('checkbox') + it('adds the name props to all Checkbox types', async () => { + await renderCheckboxGroup({ name: TEST_NAME }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes.length).toBe(2) expect(checkboxes[0]).toHaveAttribute('name', TEST_NAME) expect(checkboxes[1]).toHaveAttribute('name', TEST_NAME) }) - it('links the messages to the fieldset via aria-describedby', () => { - const { container } = renderCheckboxGroup({ + it('links the messages to the fieldset via aria-describedby', async () => { + const { container } = await renderCheckboxGroup({ messages: [{ text: TEST_ERROR_MESSAGE, type: 'error' }] }) - const group = screen.getByRole('group') + const group = page.getByRole('group').element() const ariaDesc = group.getAttribute('aria-describedby') const messageById = container.querySelector(`[id="${ariaDesc}"]`) @@ -94,8 +93,10 @@ describe('', () => { expect(messageById).toHaveTextContent(TEST_ERROR_MESSAGE) }) - it('displays description message inside the legend', () => { - const { container } = renderCheckboxGroup({ description: TEST_DESCRIPTION }) + it('displays description message inside the legend', async () => { + const { container } = await renderCheckboxGroup({ + description: TEST_DESCRIPTION + }) const legend = container.querySelector( 'span[class$="-formFieldLayout__label"]' ) @@ -106,12 +107,12 @@ describe('', () => { it('does not call the onChange prop when disabled', async () => { const onChange = vi.fn() - renderCheckboxGroup({ onChange, disabled: true }) - const checkboxElement = screen.getAllByRole('checkbox')[0] + await renderCheckboxGroup({ onChange, disabled: true }) + const checkboxElement = page.getByRole('checkbox').elements()[0] - fireEvent.click(checkboxElement) + await userEvent.click(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() expect(checkboxElement).toBeDisabled() }) @@ -119,12 +120,12 @@ describe('', () => { it('does not call the onChange prop when readOnly', async () => { const onChange = vi.fn() - renderCheckboxGroup({ onChange, readOnly: true }) - const checkboxElement = screen.getAllByRole('checkbox')[0] + await renderCheckboxGroup({ onChange, readOnly: true }) + const checkboxElement = page.getByRole('checkbox').elements()[0] - fireEvent.click(checkboxElement) + await userEvent.click(checkboxElement, { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() expect(checkboxElement).not.toBeDisabled() }) @@ -132,15 +133,15 @@ describe('', () => { it('should not update the value when the value prop is set', async () => { const onChange = vi.fn() - renderCheckboxGroup({ onChange, value: ['tester'] }) - const checkboxes = screen.getAllByRole('checkbox') + await renderCheckboxGroup({ onChange, value: ['tester'] }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).not.toBeChecked() - await userEvent.click(checkboxes[0]) + await userEvent.click(checkboxes[0], { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).toHaveBeenCalledWith(['tester', 'test-value-1']) expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).not.toBeChecked() @@ -149,26 +150,26 @@ describe('', () => { it('should add the checkbox value to the value list when it is checked', async () => { const onChange = vi.fn() - renderCheckboxGroup({ onChange }) - const checkboxes = screen.getAllByRole('checkbox') + await renderCheckboxGroup({ onChange }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).not.toBeChecked() - await userEvent.click(checkboxes[0]) - await userEvent.click(checkboxes[1]) + await userEvent.click(checkboxes[0], { force: true }) + await userEvent.click(checkboxes[1], { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(checkboxes[0]).toBeChecked() expect(checkboxes[1]).toBeChecked() expect(onChange).toHaveBeenCalledWith([TEST_VALUE_1, TEST_VALUE_2]) }) }) - it('should check the checkboxes based on the defaultValue prop', () => { + it('should check the checkboxes based on the defaultValue prop', async () => { const defaultValue = [TEST_VALUE_2] - renderCheckboxGroup({ defaultValue }) - const checkboxes = screen.getAllByRole('checkbox') + await renderCheckboxGroup({ defaultValue }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).toBeChecked() @@ -177,15 +178,15 @@ describe('', () => { it('should remove the checkbox value from the value list when it is unchecked', async () => { const onChange = vi.fn() const defaultValue = [TEST_VALUE_1, TEST_VALUE_2] - renderCheckboxGroup({ onChange, defaultValue }) - const checkboxes = screen.getAllByRole('checkbox') + await renderCheckboxGroup({ onChange, defaultValue }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes[0]).toBeChecked() expect(checkboxes[1]).toBeChecked() - await userEvent.click(checkboxes[0]) + await userEvent.click(checkboxes[0], { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).toBeChecked() expect(onChange).toHaveBeenCalledWith([TEST_VALUE_2]) @@ -195,15 +196,15 @@ describe('', () => { it('passes the array of selected values to onChange handler', async () => { const onChange = vi.fn() const defaultValue = [TEST_VALUE_2] - renderCheckboxGroup({ onChange, defaultValue }) - const checkboxes = screen.getAllByRole('checkbox') + await renderCheckboxGroup({ onChange, defaultValue }) + const checkboxes = page.getByRole('checkbox').elements() expect(checkboxes[0]).not.toBeChecked() expect(checkboxes[1]).toBeChecked() - await userEvent.click(checkboxes[0]) + await userEvent.click(checkboxes[0], { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(checkboxes[0]).toBeChecked() expect(checkboxes[1]).toBeChecked() expect(onChange).toHaveBeenCalledWith([TEST_VALUE_2, TEST_VALUE_1]) @@ -212,14 +213,14 @@ describe('', () => { describe('for a11y', () => { it('should meet standards', async () => { - const { container } = renderCheckboxGroup() + const { container } = await renderCheckboxGroup() const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) }) - it('adds the correct ARIA attributes', () => { - const { container } = renderCheckboxGroup({ + it('adds the correct ARIA attributes', async () => { + const { container } = await renderCheckboxGroup({ disabled: true, messages: [{ type: 'newError', text: 'abc' }], // @ts-ignore This is a valid attribute diff --git a/packages/ui-color-picker/src/ColorContrast/__tests__/ColorContrast.test.tsx b/packages/ui-color-picker/src/ColorContrast/__tests__/ColorContrast.test.tsx index ebee475c96..670d076906 100644 --- a/packages/ui-color-picker/src/ColorContrast/__tests__/ColorContrast.test.tsx +++ b/packages/ui-color-picker/src/ColorContrast/__tests__/ColorContrast.test.tsx @@ -22,9 +22,9 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { contrast } from '@instructure/ui-color-utils' @@ -52,7 +52,7 @@ describe('', () => { describe('elementRef prop', () => { it('should provide ref', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { describe('labels are displayed:', () => { Object.entries(testLabels).forEach(([label, text]) => { it(label, async () => { - const { container } = render( + const { container } = await render( ) @@ -78,47 +78,57 @@ describe('', () => { describe('labelLevel prop', () => { it('should render label as div when labelLevel is not provided', async () => { - render() + await render() - const heading = screen.queryByRole('heading', { - name: testLabels.label - }) + const heading = page + .getByRole('heading', { + name: testLabels.label + }) + .query() expect(heading).not.toBeInTheDocument() - const labelText = screen.getByText(testLabels.label) + const labelText = page.getByText(testLabels.label).element() expect(labelText).toBeInTheDocument() expect(labelText.tagName.toLowerCase()).toBe('div') }) it('should render label as Heading when labelLevel is provided', async () => { - render() + await render( + + ) - const heading = screen.getByRole('heading', { - name: testLabels.label, - level: 2 - }) + const heading = page + .getByRole('heading', { + name: testLabels.label, + level: 2 + }) + .element() expect(heading).toBeInTheDocument() }) it('should render correct heading level', async () => { - const { rerender } = render( + const { rerender } = await render( ) - let heading = screen.getByRole('heading', { - name: testLabels.label, - level: 3 - }) + let heading = page + .getByRole('heading', { + name: testLabels.label, + level: 3 + }) + .element() expect(heading).toBeInTheDocument() - rerender( + await rerender( ) - heading = screen.getByRole('heading', { - name: testLabels.label, - level: 1 - }) + heading = page + .getByRole('heading', { + name: testLabels.label, + level: 1 + }) + .element() expect(heading).toBeInTheDocument() }) }) @@ -128,7 +138,7 @@ describe('', () => { const color1 = '#fff' const color2 = '#088' - const { container } = render( + const { container } = await render( ', () => { const color1 = '#fff' const color2 = '#00888880' - const { container } = render( + const { container } = await render( ', () => { describe('withoutColorPreview prop', () => { it('should be false by default, should display preview', async () => { - const { container } = render( + const { container } = await render( ) @@ -171,7 +181,7 @@ describe('', () => { }) it('should hide preview', async () => { - const { container } = render( + const { container } = await render( ) @@ -195,7 +205,7 @@ describe('', () => { ) => { describe(title, () => { it(`normal text should ${expectedResult.normal.toLowerCase()}`, async () => { - const { container } = render( + const { container } = await render( ', () => { }) it(`large text should ${expectedResult.large.toLowerCase()}`, async () => { - const { container } = render( + const { container } = await render( ', () => { }) it(`graphics should ${expectedResult.graphics.toLowerCase()}`, async () => { - const { container } = render( + const { container } = await render( ', () => { describe('should be accessible', () => { it('a11y', async () => { - const { container } = render( + const { container } = await render( ) const axeCheck = await runAxeCheck(container) diff --git a/packages/ui-color-picker/src/ColorIndicator/__tests__/ColorIndicator.test.tsx b/packages/ui-color-picker/src/ColorIndicator/__tests__/ColorIndicator.test.tsx index b24b056629..b8d3507ba5 100644 --- a/packages/ui-color-picker/src/ColorIndicator/__tests__/ColorIndicator.test.tsx +++ b/packages/ui-color-picker/src/ColorIndicator/__tests__/ColorIndicator.test.tsx @@ -22,17 +22,29 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' +import { colorToRGB, colorToHex8 } from '@instructure/ui-color-utils' import { ColorIndicator } from '@instructure/ui-color-picker/latest' +const colorTestCases = { + '3 digit hex': '#069', + '6 digit hex': '#01659a', + rgb: 'rgb(100, 0, 200)', + rgba: 'rgba(100, 0, 200, .5)', + named: 'white', + hsl: 'hsl(30, 100%, 50%)', + hsla: 'hsla(30, 100%, 50%, .35)' +} + describe('', () => { describe('elementRef prop', () => { it('should provide ref', async () => { const elementRef = vi.fn() - const { container } = render() + const { container } = await render( + + ) expect(elementRef).toHaveBeenCalledWith(container.firstChild) }) @@ -40,10 +52,68 @@ describe('', () => { describe('should be accessible', () => { it('a11y', async () => { - const { container } = render() + const { container } = await render() const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) }) }) + + it('should display empty by default', async () => { + const { container } = await render() + const indicator = container.querySelector('div[class$="-colorIndicator"]')! + + expect(indicator).toBeInTheDocument() + expect(getComputedStyle(indicator).boxShadow).toBe('none') + }) + + Object.entries(colorTestCases).forEach(([testCase, testColor]) => { + it(`should display ${testCase} color`, async () => { + const expectedColor = colorToRGB(testColor) + const { container } = await render() + const indicator = container.querySelector( + 'div[class$="-colorIndicator"]' + )! + + expect(indicator).toBeInTheDocument() + + const boxShadow = getComputedStyle(indicator).boxShadow + const colorValue = boxShadow.split(')')[0] + ')' + + expect(colorToRGB(colorValue)).toEqual(expectedColor) + }) + }) + + // needs to be checked separately, the alpha is rounded different + it('should display 8 digit hexa color', async () => { + const testColor = '#06AD8580' + const expectedColor = colorToHex8(testColor) + const { container } = await render() + const indicator = container.querySelector('div[class$="-colorIndicator"]')! + + expect(indicator).toBeInTheDocument() + + const boxShadow = getComputedStyle(indicator).boxShadow + const colorValue = boxShadow.split(')')[0] + ')' + + expect(colorToHex8(colorValue)).toEqual(expectedColor) + }) + + it('should display circle by default', async () => { + const { container } = await render() + const indicator = container.querySelector('div[class$="-colorIndicator"]')! + const { borderRadius, width, height } = getComputedStyle(indicator) + + expect(width).toEqual(height) + expect(borderRadius).toEqual(width) + }) + + it('should display rectangle version', async () => { + const { container } = await render() + const indicator = container.querySelector('div[class$="-colorIndicator"]')! + const { borderRadius, width, height } = getComputedStyle(indicator) + + expect(width).toEqual(height) + expect(borderRadius).toEqual('6px') + }) }) diff --git a/packages/ui-color-picker/src/ColorMixer/__tests__/ColorMixer.test.tsx b/packages/ui-color-picker/src/ColorMixer/__tests__/ColorMixer.test.tsx index 5906950f2e..24869c1f93 100644 --- a/packages/ui-color-picker/src/ColorMixer/__tests__/ColorMixer.test.tsx +++ b/packages/ui-color-picker/src/ColorMixer/__tests__/ColorMixer.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { userEvent } from '@testing-library/user-event' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { deepEqual } from '@instructure/ui-utils' @@ -81,7 +80,7 @@ describe('', () => { describe('elementRef prop', () => { it('should provide ref', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { describe('labels are displayed:', () => { it('should render input labels', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should render explanation labels', async () => { - render( + await render( ', () => { onChange={vi.fn()} /> ) - const sliders = screen.getAllByRole('slider') - const palette = screen.getByRole('button') + const sliders = page.getByRole('slider').elements() + const palette = page.getByRole('button').element() expect(sliders[0]).toHaveAttribute( 'aria-label', @@ -146,7 +145,7 @@ describe('', () => { describe('should be accessible', () => { it('a11y', async () => { - const { container } = render( + const { container } = await render( ', () => { describe('edge cases for color value', () => { Object.entries(edgeColorValues).forEach(([label, color]) => { it(label, async () => { - render( + await render( ', () => { /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() const [r, g, b, a] = inputs.map((input) => Number(input.getAttribute('value')) ) @@ -185,7 +184,7 @@ describe('', () => { Object.entries(differentHexColorValues).forEach(([length, color]) => { it(`mount with ${length}-character hex color`, async () => { const colorInput = color - render( + await render( ', () => { /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() const [r, g, b, a] = inputs.map((input) => Number(input.getAttribute('value')) ) @@ -206,7 +205,7 @@ describe('', () => { }) it('mount with invalid hex color', async () => { - render( + await render( ', () => { /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() const [r, g, b, a] = inputs.map((input: any) => Number(input.getAttribute('value')) ) const colorHex = conversions.colorToHex8({ r, g, b, a }) expect(colorHex).toBe('#000000FF') - await waitFor(() => { - expect(consoleWarningMock.mock.calls[0][0]).toEqual( + await vi.waitFor(() => { + expect(consoleWarningMock).toHaveBeenCalledWith( expect.stringContaining( 'Warning: [ColorMixer] The passed color value is not valid.' - ) + ), + expect.anything() ) }) }) @@ -236,7 +236,7 @@ describe('', () => { describe('hue slider', () => { it('should not call onChange when the `tab` key is pressed', async () => { const onChange = vi.fn() - render( + await render( ', () => { /> ) - const colorSlider = screen.getByRole('slider', { - name: testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel - }) + const colorSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel + }) + .element() colorSlider.focus() await userEvent.tab() - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() }) }) it('onChange should not be call when component is disabled', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const colorSlider = screen.getByRole('slider', { - name: testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel - }) + const colorSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel + }) + .element() await userEvent.type(colorSlider, '{arrowright}') - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() }) }) @@ -282,7 +286,7 @@ describe('', () => { describe('alpha slider', () => { it('should not call onChange when a `tab` key press is received', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const alphaSlider = screen.getByRole('slider', { - name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel - }) + const alphaSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel + }) + .element() alphaSlider.focus() await userEvent.tab() - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() }) }) it('should not call onChange when the component is disabled', async () => { const onChange = vi.fn() - render( + await render( ', () => { /> ) - const alphaSlider = screen.getByRole('slider', { - name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel - }) + const alphaSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel + }) + .element() await userEvent.type(alphaSlider, '{arrowright}') - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() }) }) @@ -329,7 +337,7 @@ describe('', () => { it('the alpha slider does not show when withAlpha is false', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const alphaSlider = screen.queryByRole('slider', { - name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel - }) + const alphaSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel + }) + .query() expect(alphaSlider).not.toBeInTheDocument() }) @@ -348,7 +358,7 @@ describe('', () => { it('the alpha slider does not show when withAlpha is not set', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const alphaSlider = screen.queryByRole('slider', { - name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel - }) + const alphaSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel + }) + .query() expect(alphaSlider).not.toBeInTheDocument() }) @@ -366,7 +378,7 @@ describe('', () => { it('should set the disabled attribute when `disabled` is set', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const alphaSlider = screen.queryByRole('slider', { - name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel - }) + const alphaSlider = page + .getByRole('slider', { + name: testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel + }) + .query() expect(alphaSlider).toHaveAttribute('disabled') }) @@ -388,7 +402,7 @@ describe('', () => { it('should set the disabled attribute when `disabled` is set', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const colorPalette = screen.queryByRole('button', { - name: testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel - }) + const colorPalette = page + .getByRole('button', { + name: testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel + }) + .query() expect(colorPalette).toHaveAttribute('disabled') }) @@ -408,7 +424,7 @@ describe('', () => { describe('color input', () => { it('the alpha input exsits when `withAlpha` is set', async () => { - const { container } = render( + const { container } = await render( ', () => { const alphaInput = container.querySelector( 'span[class$="-RGBAInput__aInput"]' ) - const alphaInputScreenReaderLabel = screen.getByText( - testInputLabels.rgbAlphaInputScreenReaderLabel - ) + const alphaInputScreenReaderLabel = page + .getByText(testInputLabels.rgbAlphaInputScreenReaderLabel) + .element() expect(alphaInput).toBeInTheDocument() expect(alphaInputScreenReaderLabel).toBeInTheDocument() }) it('the alpha input does not exsit when `withAlpha` is not set', async () => { - const { container } = render( + const { container } = await render( ', () => { const alphaInput = container.querySelector( 'span[class$="-RGBAInput__aInput"]' ) - const alphaInputScreenReaderLabel = screen.queryByText( - testInputLabels.rgbAlphaInputScreenReaderLabel - ) + const alphaInputScreenReaderLabel = page + .getByText(testInputLabels.rgbAlphaInputScreenReaderLabel) + .query() expect(alphaInput).not.toBeInTheDocument() expect(alphaInputScreenReaderLabel).not.toBeInTheDocument() @@ -451,7 +467,7 @@ describe('', () => { it('should not call onChange when `disabled` is set and get the input', async () => { const fakeValue = '234234' const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(4) await userEvent.type(inputs[0], fakeValue) @@ -468,13 +484,13 @@ describe('', () => { await userEvent.type(inputs[2], fakeValue) await userEvent.type(inputs[3], fakeValue) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).not.toHaveBeenCalled() }) }) it('should set the disabled attribute when `disabled` and `withAlpha` is set', async () => { - render( + await render( ', () => { onChange={vi.fn()} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(4) inputs.forEach((input) => { @@ -492,7 +508,7 @@ describe('', () => { }) it('should set the disabled attribute when `disabled` is set', async () => { - render( + await render( ', () => { onChange={vi.fn()} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(3) inputs.forEach((input) => { @@ -510,7 +526,7 @@ describe('', () => { it('should not accept letter character', async () => { const invalidColor = 'adfafas' - render( + await render( ', () => { onChange={vi.fn()} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(4) await userEvent.type(inputs[0], invalidColor) @@ -526,7 +542,7 @@ describe('', () => { await userEvent.type(inputs[2], invalidColor) await userEvent.type(inputs[3], invalidColor) - await waitFor(() => { + await vi.waitFor(() => { expect(inputs[0]).not.toHaveValue(invalidColor) expect(inputs[1]).not.toHaveValue(invalidColor) expect(inputs[2]).not.toHaveValue(invalidColor) @@ -536,7 +552,7 @@ describe('', () => { it('should not accept negative value', async () => { const invalidColor = '-10' - render( + await render( ', () => { onChange={vi.fn()} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(4) await userEvent.type(inputs[0], invalidColor) @@ -552,7 +568,7 @@ describe('', () => { await userEvent.type(inputs[2], invalidColor) await userEvent.type(inputs[3], invalidColor) - await waitFor(() => { + await vi.waitFor(() => { expect(inputs[0]).not.toHaveValue(invalidColor) expect(inputs[1]).not.toHaveValue(invalidColor) expect(inputs[2]).not.toHaveValue(invalidColor) @@ -562,21 +578,21 @@ describe('', () => { it('should not accept value that bigger than 255', async () => { const invalidColor = '300' - render( + await render( ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(3) await userEvent.type(inputs[0], invalidColor) await userEvent.type(inputs[1], invalidColor) await userEvent.type(inputs[2], invalidColor) - await waitFor(() => { + await vi.waitFor(() => { expect(inputs[0]).not.toHaveValue(invalidColor) expect(inputs[1]).not.toHaveValue(invalidColor) expect(inputs[2]).not.toHaveValue(invalidColor) @@ -585,7 +601,7 @@ describe('', () => { it('for alpha input, should not accept value that bigger than 100', async () => { const invalidColor = '101' - render( + await render( ', () => { onChange={vi.fn()} /> ) - const inputs = screen.getAllByRole('textbox') + const inputs = page.getByRole('textbox').elements() expect(inputs.length).toBe(4) await userEvent.type(inputs[3], invalidColor) - await waitFor(() => { + await vi.waitFor(() => { expect(inputs[3]).not.toHaveValue(invalidColor) }) }) }) + + const hueSlider = () => + document.querySelector( + `[role="slider"][aria-label="${testScreenReaderLabels.colorSliderNavigationExplanationScreenReaderLabel}"]` + )! + + const alphaSlider = () => + document.querySelector( + `[role="slider"][aria-label="${testScreenReaderLabels.alphaSliderNavigationExplanationScreenReaderLabel}"]` + )! + + const palette = () => + document.querySelector( + `[role="button"][aria-label="${testScreenReaderLabels.colorPaletteNavigationExplanationScreenReaderLabel}"]` + )! + + const sliderIndicator = (slider: HTMLElement) => + slider.querySelector('[class*=-colorMixerSlider__indicator]')! + + const paletteIndicator = () => + palette().querySelector('[class*=-ColorPalette__indicator]')! + + // the component maps pointer coordinates against this inner element, so + // clicks are positioned relative to it + const paletteSurface = () => + document.querySelector('[class*=-ColorPalette__palette]')! + + it('should change hue value when click at the middle of the slider', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = hueSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { + position: { x: rect.width / 2, y: rect.height / 2 } + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalled() + }) + }) + + it('should change hue value when click at the end of the slider', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = hueSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { + position: { x: rect.width - 1, y: rect.height / 2 } + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalled() + }) + }) + + it('should change hue value when click at the beginning of the slider', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = hueSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { + position: { x: 1, y: rect.height / 2 } + }) + + // Because we already passed the `value` that different from the default value then the component changes their color once, so if we want to test with any action after that, `onChange` should be called twice. + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + + for (const key of ['a', 'd', '{ArrowLeft}', '{ArrowRight}']) { + it(`should hue value change with '${key}' key pressed`, async () => { + const onChange = vi.fn() + await render( + + ) + + hueSlider().focus() + await userEvent.keyboard(key) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + } + + it('the hue indicator move left', async () => { + await render( + + ) + const slider = hueSlider() + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('a') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toBeLessThan(pos1) + + await userEvent.keyboard('{ArrowLeft}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toBeLessThan(pos2) + }) + + it('the hue indicator move right', async () => { + await render( + + ) + const slider = hueSlider() + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('d') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toBeGreaterThan(pos1) + + await userEvent.keyboard('{ArrowRight}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toBeGreaterThan(pos2) + }) + + it('should not move the hue indicator when reach the left border', async () => { + await render( + + ) + const slider = hueSlider() + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('a') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toEqual(pos1) + + await userEvent.keyboard('{ArrowLeft}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toEqual(pos2) + }) + + it('should not move the hue indicator when reach the right border', async () => { + await render( + + ) + const slider = hueSlider() + + // Initial positioning to reach the right end of the slider + slider.focus() + await userEvent.keyboard('{ArrowRight}') + await userEvent.keyboard('{ArrowRight}') + + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('d') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toEqual(pos1) + + await userEvent.keyboard('{ArrowRight}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toEqual(pos2) + }) + + it('should change alpha value when click at the beginning of the bar', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = alphaSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { position: { x: 1, y: rect.height / 2 } }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + + it('should change alpha value when click at the end of the bar', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = alphaSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { + position: { x: rect.width - 1, y: rect.height / 2 } + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + + it('should change alpha value when click at the middle of the slider', async () => { + const onChange = vi.fn() + await render( + + ) + const slider = alphaSlider() + const rect = slider.getBoundingClientRect() + + await userEvent.click(slider, { + position: { x: rect.width / 2, y: rect.height / 2 } + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + + for (const key of ['{ArrowRight}', '{ArrowLeft}', 'a', 'd']) { + it(`should alpha value change with '${key}' key pressed`, async () => { + const onChange = vi.fn() + await render( + + ) + + alphaSlider().focus() + await userEvent.keyboard(key) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + } + + it('should not move the alpha indicator when reach the left border', async () => { + await render( + + ) + const slider = alphaSlider() + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('a') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toEqual(pos1) + + await userEvent.keyboard('{ArrowLeft}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toEqual(pos2) + }) + + it('should not move the alpha indicator when reach the right border', async () => { + await render( + + ) + const slider = alphaSlider() + const indicator = sliderIndicator(slider) + const pos1 = indicator.getBoundingClientRect().x + + slider.focus() + await userEvent.keyboard('d') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toEqual(pos1) + + await userEvent.keyboard('{ArrowRight}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toEqual(pos2) + }) + + it('should palette change the color when mousedown event is received inside the palette', async () => { + const onChange = vi.fn() + await render( + + ) + const surface = paletteSurface() + const rect = surface.getBoundingClientRect() + + await userEvent.click(surface, { + position: { x: rect.width / 2, y: rect.height / 2 } + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith('#783A3AFF') + }) + }) + + const paletteClickPositions: [ + string, + (rect: DOMRect) => { x: number; y: number } + ][] = [ + ['the top border', (rect) => ({ x: rect.width / 2, y: 1 })], + [ + 'the bottom border', + (rect) => ({ x: rect.width / 2, y: rect.height - 4 }) + ], + ['the left border', (rect) => ({ x: 1, y: rect.height / 2 })], + ['the right border', (rect) => ({ x: rect.width - 2, y: rect.height / 2 })], + ['the top left corner', () => ({ x: 1, y: 1 })], + ['the top right corner', (rect) => ({ x: rect.width - 2, y: 1 })], + [ + 'the bottom right corner', + (rect) => ({ x: rect.width - 4, y: rect.height - 4 }) + ], + ['the bottom left corner', (rect) => ({ x: 2, y: rect.height - 2 })] + ] + + for (const [name, getPosition] of paletteClickPositions) { + it(`should palette change the color when mousedown event is received at ${name}`, async () => { + const onChange = vi.fn() + await render( + + ) + const surface = paletteSurface() + + await userEvent.click(surface, { + position: getPosition(surface.getBoundingClientRect()) + }) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalled() + }) + }) + } + + for (const key of [ + 'a', + 'w', + 's', + 'd', + '{ArrowLeft}', + '{ArrowRight}', + '{ArrowUp}', + '{ArrowDown}' + ]) { + it(`should onChange is call when the palette receive event from keyboard '${key}'`, async () => { + const onChange = vi.fn() + await render( + + ) + + palette().focus() + await userEvent.keyboard(key) + + // use `toHaveBeenCalledTimes(2)` because it is called first time when passing `testValue` color + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(2) + }) + }) + } + + it('should palette indicator moves up when receive the ArrowUp or w keyboard event', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().y + + palette().focus() + await userEvent.keyboard('w') + const pos2 = indicator.getBoundingClientRect().y + expect(pos2).toBeLessThan(pos1) + + await userEvent.keyboard('{ArrowUp}') + const pos3 = indicator.getBoundingClientRect().y + expect(pos3).toBeLessThan(pos2) + }) + + it('should palette indicator moves down when receive the ArrowDown or s keyboard event', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().y + + palette().focus() + await userEvent.keyboard('s') + const pos2 = indicator.getBoundingClientRect().y + expect(pos2).toBeGreaterThan(pos1) + + await userEvent.keyboard('{ArrowDown}') + const pos3 = indicator.getBoundingClientRect().y + expect(pos3).toBeGreaterThan(pos2) + }) + + it('should palette indicator moves left when receive the ArrowLeft or a keyboard event', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().x + + palette().focus() + await userEvent.keyboard('a') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toBeLessThan(pos1) + + await userEvent.keyboard('{ArrowLeft}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toBeLessThan(pos2) + }) + + it('should palette indicator moves right when receive the ArrowRight or d keyboard event', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().x + + palette().focus() + await userEvent.keyboard('d') + const pos2 = indicator.getBoundingClientRect().x + expect(pos2).toBeGreaterThan(pos1) + + await userEvent.keyboard('{ArrowRight}') + const pos3 = indicator.getBoundingClientRect().x + expect(pos3).toBeGreaterThan(pos2) + }) + + it('should palette indicator does not move up when it reach the top border', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().y + + palette().focus() + await userEvent.keyboard('w') + expect(indicator.getBoundingClientRect().y).toEqual(pos1) + + await userEvent.keyboard('{ArrowUp}') + expect(indicator.getBoundingClientRect().y).toEqual(pos1) + }) + + it('should palette indicator does not move down when it reach the bottom border', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().y + + palette().focus() + await userEvent.keyboard('s') + expect(indicator.getBoundingClientRect().y).toEqual(pos1) + + await userEvent.keyboard('{ArrowDown}') + expect(indicator.getBoundingClientRect().y).toEqual(pos1) + }) + + it('should palette indicator does not move left when it reach the left border', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().x + + palette().focus() + await userEvent.keyboard('a') + expect(indicator.getBoundingClientRect().x).toEqual(pos1) + + await userEvent.keyboard('{ArrowLeft}') + expect(indicator.getBoundingClientRect().x).toEqual(pos1) + }) + + it('should palette indicator does not move right when it reach the right border', async () => { + await render( + + ) + const indicator = paletteIndicator() + const pos1 = indicator.getBoundingClientRect().x + + palette().focus() + await userEvent.keyboard('d') + expect(indicator.getBoundingClientRect().x).toEqual(pos1) + + await userEvent.keyboard('{ArrowRight}') + expect(indicator.getBoundingClientRect().x).toEqual(pos1) + }) }) diff --git a/packages/ui-color-picker/src/ColorPicker/__tests__/ColorPicker.test.tsx b/packages/ui-color-picker/src/ColorPicker/__tests__/ColorPicker.test.tsx index edde8350d9..7bc20a956b 100644 --- a/packages/ui-color-picker/src/ColorPicker/__tests__/ColorPicker.test.tsx +++ b/packages/ui-color-picker/src/ColorPicker/__tests__/ColorPicker.test.tsx @@ -22,18 +22,87 @@ * SOFTWARE. */ -import { render, screen, waitFor, fireEvent } from '@testing-library/react' -import { userEvent } from '@testing-library/user-event' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { fireEvent } from '@testing-library/dom' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' -import conversions from '@instructure/ui-color-utils' +import conversions, { colorToRGB, color2hex } from '@instructure/ui-color-utils' +import { Button } from '@instructure/ui-buttons/latest' import { ContrastStrength } from '../v2/props.js' -import { ColorPicker } from '@instructure/ui-color-picker/latest' +import { + ColorPicker, + ColorMixer, + ColorPreset, + ColorContrast +} from '@instructure/ui-color-picker/latest' import type { ColorPickerProps } from '@instructure/ui-color-picker/latest' +const colorPreset = [ + '#ffffff', + '#0CBF94', + '#0C89BF', + '#BF0C6D', + '#BF8D0C', + '#ff0000', + '#576A66', + '#35423A', + '#35423F' +] + +const colorMixerSettings = { + popoverAddButtonLabel: 'add', + popoverCloseButtonLabel: 'close', + colorMixer: { + withAlpha: false, + rgbRedInputScreenReaderLabel: 'Red input', + rgbBlueInputScreenReaderLabel: 'Blue input', + colorSliderNavigationExplanationScreenReaderLabel: '', + colorPaletteNavigationExplanationScreenReaderLabel: '', + rgbAlphaInputScreenReaderLabel: '', + alphaSliderNavigationExplanationScreenReaderLabel: '', + rgbGreenInputScreenReaderLabel: 'Green input' + } +} + +const presetIndicators = () => + Array.from( + document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + ) + +const indicatorColor = (indicator: Element) => { + const boxShadow = getComputedStyle(indicator).boxShadow + + return colorToRGB(boxShadow.split(')')[0] + ')') +} + +// Resolve the RGBA from its (screen-reader) label via the label's +// `for` association, instead of relying on the TextInput's internal DOM nesting +const rgbaInput = (labelText: string) => { + const label = Array.from(document.querySelectorAll('label')).find( + (candidate) => candidate.textContent?.startsWith(labelText) + )! + + return document.getElementById(label.htmlFor) as HTMLInputElement +} + +// real browsers only accept a DataTransfer as `clipboardData` +const pasteInto = (input: HTMLInputElement, text: string) => { + const clipboardData = new DataTransfer() + clipboardData.setData('text/plain', text) + input.dispatchEvent( + new ClipboardEvent('paste', { + clipboardData, + bubbles: true, + cancelable: true + }) + ) +} + const SimpleExample = (props: Partial) => { return ( ', () => { describe('simple input mode', () => { it('should render correctly', async () => { - const { container } = render() + const { container } = await render() expect(container.firstChild).toBeInTheDocument() }) @@ -75,80 +144,82 @@ describe('', () => { const color = '#FFF' const onChange = vi.fn() - const { rerender } = render( + const { rerender } = await render( ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() expect(input).toHaveValue('FFF') // set new value - rerender() + await rerender( + + ) - const inputUpdated = screen.getByRole('textbox') + const inputUpdated = page.getByRole('textbox').element() expect(inputUpdated).toHaveValue('FFF555') }) it('should accept 3 digit hex code', async () => { const color = '0CB' - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, color) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue(color) }) }) it('should accept 6 digit hex code', async () => { const color = '0CBF2D' - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, color) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue(color) }) }) it('should not accept not valid hex code', async () => { const color = 'WWWZZZ' - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, color) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).not.toHaveValue(color) }) }) it('should not allow more than 6 characters', async () => { const color = '0CBF2D1234567' - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, color) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('0CBF2D') }) }) it('should not allow input when disabled', async () => { - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() expect(input).toHaveAttribute('disabled') }) @@ -160,7 +231,7 @@ describe('', () => { it(`should check contrast correctly when color has enough contrast [contrastStrength=${contrastStrength}]`, async () => { //oxford in canvas color palette, should be valid with all contrast strenght checkers const colorToCheck = '394B58' - const { container } = render( + const { container } = await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue(colorToCheck) const successIconWrapper = container.querySelector( @@ -189,7 +260,7 @@ describe('', () => { it(`should check contrast correctly when color does not have enough contrast [contrastStrength=${contrastStrength}, isStrict=false]`, async () => { //porcelain in canvas color palette, it should be failing even the min check const colorToCheck = 'F5F5F5' - const { container } = render( + const { container } = await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue(colorToCheck) const warningIconWrapper = container.querySelector( @@ -218,7 +289,7 @@ describe('', () => { it(`should check contrast correctly when color does not have enough contrast [contrastStrength=${contrastStrength}, isStrict=true]`, async () => { //porcelain in canvas color palette, it should be failing even the min check const colorToCheck = 'F5F5F5' - const { container } = render( + const { container } = await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue(colorToCheck) const errorIconWrapper = container.querySelector( @@ -246,7 +317,7 @@ describe('', () => { it(`should display success message when contrast is met [contrastStrength=${contrastStrength}]`, async () => { const colorToCheck = '394B58' - render( + await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { - const successMessage = screen.getByText( - 'I am a contrast success message' - ) + await vi.waitFor(() => { + const successMessage = page + .getByText('I am a contrast success message') + .element() expect(input).toHaveValue(colorToCheck) expect(successMessage).toBeInTheDocument() @@ -274,7 +345,7 @@ describe('', () => { it(`should display error message when contrast is not met [contrastStrength=${contrastStrength}, isStrict=false]`, async () => { const colorToCheck = 'F5F5F5' - render( + await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { - const warningMessage = screen.getByText( - 'I am a contrast warning message' - ) + await vi.waitFor(() => { + const warningMessage = page + .getByText('I am a contrast warning message') + .element() expect(input).toHaveValue(colorToCheck) expect(warningMessage).toBeInTheDocument() @@ -302,7 +373,7 @@ describe('', () => { it(`should display error message when contrast is not met [contrastStrength=${contrastStrength}, isStrict=true]`, async () => { const colorToCheck = 'F5F5F5' - render( + await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, colorToCheck) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { - const errorMessage = screen.getByText('I am a contrast error message') + await vi.waitFor(() => { + const errorMessage = page + .getByText('I am a contrast error message') + .element() expect(input).toHaveValue(colorToCheck) expect(errorMessage).toBeInTheDocument() @@ -329,20 +402,20 @@ describe('', () => { it('should call onChange', async () => { const onChange = vi.fn() - render() + await render() - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() fireEvent.change(input, { target: { value: 'FFF' } }) - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).toHaveBeenLastCalledWith('#FFF') }) }) it('should display message when ColorPicker is a required field', async () => { - render( + await render( [ @@ -353,33 +426,37 @@ describe('', () => { ]} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() - fireEvent.focus(input) - fireEvent.blur(input) + fireEvent.focusIn(input) + fireEvent.focusOut(input) - await waitFor(() => { - const requiredMessage = screen.getByText('I am a required message') + await vi.waitFor(() => { + const requiredMessage = page + .getByText('I am a required message') + .element() expect(requiredMessage).toBeInTheDocument() }) }) it('should display message when color is invalid', async () => { - render( + await render( [ { type: 'error', text: 'I am an invalid color message' } ]} /> ) - const input = screen.getByRole('textbox') + const input = page.getByRole('textbox').element() await userEvent.type(input, 'F') - fireEvent.blur(input) + fireEvent.focusOut(input) - await waitFor(() => { - const errorMessage = screen.getByText('I am an invalid color message') + await vi.waitFor(() => { + const errorMessage = page + .getByText('I am an invalid color message') + .element() expect(errorMessage).toBeInTheDocument() }) @@ -387,134 +464,118 @@ describe('', () => { it('should provide an inputRef prop', async () => { const inputRef = vi.fn() - render() - const input = screen.getByRole('textbox') + await render() + const input = page.getByRole('textbox').element() expect(inputRef).toHaveBeenCalledWith(input) }) describe('paste behavior', () => { it('should strip leading # from pasted value', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement - fireEvent.paste(input, { - clipboardData: { getData: () => '#FF0000' } - }) + pasteInto(input, '#FF0000') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('FF0000') }) }) it('should block pasted value that exceeds 6 characters', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement - fireEvent.paste(input, { - clipboardData: { getData: () => 'FF00001' } - }) + pasteInto(input, 'FF00001') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('') }) }) it('should block pasted value with invalid hex characters', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement - fireEvent.paste(input, { - clipboardData: { getData: () => 'ZZZZZZ' } - }) + pasteInto(input, 'ZZZZZZ') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('') }) }) it('should replace entirely selected text when pasting', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement fireEvent.change(input, { target: { value: 'FF0000' } }) - await waitFor(() => expect(input).toHaveValue('FF0000')) + await vi.waitFor(() => expect(input).toHaveValue('FF0000')) input.setSelectionRange(0, 6) - fireEvent.paste(input, { - clipboardData: { getData: () => 'AABBCC' } - }) + pasteInto(input, 'AABBCC') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('AABBCC') }) }) it('should replace partially selected text when pasting', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement fireEvent.change(input, { target: { value: 'FF0000' } }) - await waitFor(() => expect(input).toHaveValue('FF0000')) + await vi.waitFor(() => expect(input).toHaveValue('FF0000')) // select the two middle zeros (positions 2–4), paste FF → FFFF00 input.setSelectionRange(2, 4) - fireEvent.paste(input, { - clipboardData: { getData: () => 'FF' } - }) + pasteInto(input, 'FF') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('FFFF00') }) }) it('should insert pasted text at cursor start position', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement fireEvent.change(input, { target: { value: '0000' } }) - await waitFor(() => expect(input).toHaveValue('0000')) + await vi.waitFor(() => expect(input).toHaveValue('0000')) input.setSelectionRange(0, 0) - fireEvent.paste(input, { - clipboardData: { getData: () => 'FF' } - }) + pasteInto(input, 'FF') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('FF0000') }) }) it('should insert pasted text at cursor middle position', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement fireEvent.change(input, { target: { value: 'FF00' } }) - await waitFor(() => expect(input).toHaveValue('FF00')) + await vi.waitFor(() => expect(input).toHaveValue('FF00')) input.setSelectionRange(2, 2) - fireEvent.paste(input, { - clipboardData: { getData: () => 'AB' } - }) + pasteInto(input, 'AB') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('FFAB00') }) }) it('should insert pasted text at cursor end position', async () => { - render() - const input = screen.getByRole('textbox') as HTMLInputElement + await render() + const input = page.getByRole('textbox').element() as HTMLInputElement fireEvent.change(input, { target: { value: '0000' } }) - await waitFor(() => expect(input).toHaveValue('0000')) + await vi.waitFor(() => expect(input).toHaveValue('0000')) input.setSelectionRange(4, 4) - fireEvent.paste(input, { - clipboardData: { getData: () => 'FF' } - }) + pasteInto(input, 'FF') - await waitFor(() => { + await vi.waitFor(() => { expect(input).toHaveValue('0000FF') }) }) @@ -523,7 +584,7 @@ describe('', () => { describe('complex mode', () => { it('should display trigger button', async () => { - const { container } = render( + const { container } = await render( ', () => { const buttonWrapper = container.querySelector( 'div[class$="-colorPicker__colorMixerButtonWrapper"]' ) - const button = screen.getByRole('button') + const button = page.getByRole('button').element() expect(buttonWrapper).toBeInTheDocument() expect(button).toBeInTheDocument() }) it('should open popover when trigger is clicked', async () => { - render( + await render( ', () => { }} /> ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() expect(trigger).toBeInTheDocument() expect(trigger).toHaveAttribute('aria-expanded', 'false') fireEvent.click(trigger) - await waitFor(() => { - const buttons = screen.getAllByRole('button') + await vi.waitFor(() => { + const buttons = page.getByRole('button').elements() const popoverContent = document.querySelector( 'div[class$="-colorPicker__popoverContent"]' ) @@ -572,7 +633,7 @@ describe('', () => { }) it('should display the color mixer', async () => { - render( + await render( ', () => { }} /> ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() fireEvent.click(trigger) - await waitFor(() => { - const redInput = screen.getByLabelText('Red input') - const blueInput = screen.getByLabelText('Blue input') - const greenInput = screen.getByLabelText('Green input') + await vi.waitFor(() => { + const redInput = page.getByLabelText('Red input').element() + const blueInput = page.getByLabelText('Blue input').element() + const greenInput = page.getByLabelText('Green input').element() expect(redInput).toBeInTheDocument() expect(blueInput).toBeInTheDocument() @@ -607,7 +668,7 @@ describe('', () => { it('should display the correct color in the colormixer when the input is prefilled', async () => { const color = '0374B5' - render( + await render( ', () => { }} /> ) - const input = screen.getByRole('textbox') - const trigger = screen.getByRole('button') + const input = page.getByRole('textbox').element() + const trigger = page.getByRole('button').element() await userEvent.type(input, color) - fireEvent.blur(input) + fireEvent.focusOut(input) fireEvent.click(trigger) - await waitFor(() => { - const redInput = screen.getByLabelText('Red input') as HTMLInputElement - const blueInput = screen.getByLabelText( - 'Blue input' - ) as HTMLInputElement - const greenInput = screen.getByLabelText( - 'Green input' - ) as HTMLInputElement + await vi.waitFor(() => { + const redInput = page + .getByLabelText('Red input') + .element() as HTMLInputElement + const blueInput = page + .getByLabelText('Blue input') + .element() as HTMLInputElement + const greenInput = page + .getByLabelText('Green input') + .element() as HTMLInputElement const convertedColor = conversions.colorToRGB(`#${color}`) const actualColor = { @@ -656,7 +719,7 @@ describe('', () => { it('should trigger onChange when selected color is added from colorMixer', async () => { const onChange = vi.fn() const rgb = { r: 131, g: 6, b: 25, a: 1 } - render( + await render( ', () => { /> ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() fireEvent.click(trigger) - await waitFor(() => { - const addBtn = screen.getByRole('button', { name: 'add' }) - const redInput = screen.getByLabelText('Red input') as HTMLInputElement - const greenInput = screen.getByLabelText( - 'Green input' - ) as HTMLInputElement - const blueInput = screen.getByLabelText( - 'Blue input' - ) as HTMLInputElement + await vi.waitFor(() => { + const addBtn = page.getByRole('button', { name: 'add' }).element() + const redInput = page + .getByLabelText('Red input') + .element() as HTMLInputElement + const greenInput = page + .getByLabelText('Green input') + .element() as HTMLInputElement + const blueInput = page + .getByLabelText('Blue input') + .element() as HTMLInputElement fireEvent.change(redInput, { target: { value: `${rgb.r}` } }) fireEvent.change(greenInput, { target: { value: `${rgb.g}` } }) @@ -703,7 +768,7 @@ describe('', () => { describe('custom popover mode', () => { it('should throw warning if children and settings object are passed too', async () => { - render( + await render( ', () => { ) - await waitFor(() => { - expect(consoleWarningMock.mock.calls[0][0]).toEqual( - expect.stringContaining( - 'Warning: You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.' + await vi.waitFor(() => { + expect( + consoleWarningMock.mock.calls.some((call: unknown[]) => + String(call[0]).includes( + 'Warning: You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.' + ) ) - ) + ).toBe(true) }) }) it('should display trigger button', async () => { - render({() =>
}
) + await render({() =>
}
) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() expect(trigger).toBeInTheDocument() expect(trigger).toHaveAttribute('data-popover-trigger', 'true') }) @@ -734,10 +801,380 @@ describe('', () => { describe('should be accessible', () => { it('a11y', async () => { - const { container } = render() + const { container } = await render() const axeCheck = await runAxeCheck(container) expect(axeCheck).toBe(true) }) }) + + it('should display the color which was typed in simple input mode', async () => { + const testColor = '0CBF2D' + await render() + + await userEvent.fill(page.getByRole('textbox'), testColor) + + const indicator = document.querySelector('div[class$="-colorIndicator"]')! + + expect(indicator).toBeInTheDocument() + expect(indicatorColor(indicator)).toEqual(colorToRGB(testColor)) + }) + + it('should display the color in the trigger button in complex mode', async () => { + const testColor = '0374B5' + await render() + + await userEvent.fill(page.getByRole('textbox'), testColor) + + const indicator = document.querySelector('div[class$="-colorIndicator"]')! + + expect(indicator).toBeInTheDocument() + expect(indicatorColor(indicator)).toEqual(colorToRGB(testColor)) + }) + + it('should display the list of colors passed to it in complex mode', async () => { + await render( + + ) + + await userEvent.click(page.getByRole('button')) + + const indicators = document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + + expect(indicators.length).toBe(colorPreset.length) + + indicators.forEach((indicator, index) => { + expect(indicatorColor(indicator)).toEqual(colorToRGB(colorPreset[index])) + }) + }) + + it('should correctly set the color when picked from the list of colors in complex mode', async () => { + await render( + + ) + + await userEvent.click(page.getByRole('button')) + await userEvent.click(presetIndicators()[1]) + await userEvent.click(page.getByRole('button', { name: 'add' })) + + await expect + .element(page.getByRole('textbox')) + .toHaveValue(colorPreset[1].substring(1)) + }) + + it('should correctly call onChange with the color when picked from the list of colors in complex mode', async () => { + const onChange = vi.fn() + await render( + + ) + + await userEvent.click(page.getByRole('button')) + await userEvent.click(presetIndicators()[1]) + await userEvent.click(page.getByRole('button', { name: 'add' })) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith(colorPreset[1]) + }) + }) + + it('should display the text passed to ColorContrast in complex mode', async () => { + await render( + + ) + + await userEvent.click(page.getByRole('button')) + + const colorContrast = document.querySelector('div[class$="-colorContrast"]') + + expect(colorContrast).toHaveTextContent('Normal text') + expect(colorContrast).toHaveTextContent('Large text') + expect(colorContrast).toHaveTextContent('Graphics text') + }) + + it('should display the correct color in the colormixer when the input is prefilled in custom popover mode', async () => { + const testColor = '0374B5' + const expectedColor = colorToRGB(`#${testColor}`) + + await render( + + {(value, onChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.fill(page.getByRole('textbox'), testColor) + await userEvent.click(page.getByRole('button')) + + await vi.waitFor(() => { + expect(rgbaInput('Input field for red')).toHaveValue(`${expectedColor.r}`) + expect(rgbaInput('Input field for green')).toHaveValue( + `${expectedColor.g}` + ) + expect(rgbaInput('Input field for blue')).toHaveValue( + `${expectedColor.b}` + ) + expect(rgbaInput('Input field for alpha')).toHaveValue('100') + }) + }) + + it('should trigger onChange when selected color is added from colorMixer in custom popover mode', async () => { + const onChange = vi.fn() + const rgb = { r: 131, g: 6, b: 25, a: 1 } + + await render( + + {(value, mixerOnChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.click(page.getByRole('button')) + + await userEvent.fill(rgbaInput('Input field for red'), `${rgb.r}`) + await userEvent.fill(rgbaInput('Input field for green'), `${rgb.g}`) + await userEvent.fill(rgbaInput('Input field for blue'), `${rgb.b}`) + + await userEvent.click(page.getByRole('button', { name: 'add' })) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith(color2hex(rgb)) + }) + }) + + it('should display the color in the trigger button in custom popover mode', async () => { + const testColor = '0374B5' + + await render( + + {(value, onChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.fill(page.getByRole('textbox'), testColor) + + const indicator = document.querySelector('div[class$="-colorIndicator"]')! + + expect(indicator).toBeInTheDocument() + expect(indicatorColor(indicator)).toEqual(colorToRGB(testColor)) + }) + + it('should display the list of colors passed to it in custom popover mode', async () => { + await render( + + {(value, onChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.click(page.getByRole('button')) + + const indicators = presetIndicators() + + expect(indicators.length).toBe(colorPreset.length) + + indicators.forEach((indicator, index) => { + expect(indicatorColor(indicator)).toEqual(colorToRGB(colorPreset[index])) + }) + }) + + it('should correctly set the color when picked from the list of colors in custom popover mode', async () => { + await render( + + {(value, onChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.click(page.getByRole('button')) + await userEvent.click(presetIndicators()[3]) + await userEvent.click(page.getByRole('button', { name: 'add' })) + + await expect + .element(page.getByRole('textbox')) + .toHaveValue(colorPreset[3].substring(1)) + }) + + it('should correctly call onChange with the color when picked from the list of colors in custom popover mode', async () => { + const onChange = vi.fn() + + await render( + + {(value, presetOnChange, handleAdd, handleClose) => ( +
+ +
+ + +
+
+ )} +
+ ) + + await userEvent.click(page.getByRole('button')) + await userEvent.click(presetIndicators()[3]) + await userEvent.click(page.getByRole('button', { name: 'add' })) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith(colorPreset[3]) + }) + }) + + it('should display the text passed to ColorContrast in custom popover mode', async () => { + await render( + + {(value, onChange, handleAdd, handleClose) => ( +
+ + +
+ + +
+
+ )} +
+ ) + + await userEvent.click(page.getByRole('button')) + + const colorContrast = document.querySelector('div[class$="-colorContrast"]') + + expect(colorContrast).toHaveTextContent('Normal text') + expect(colorContrast).toHaveTextContent('Large text') + expect(colorContrast).toHaveTextContent('Graphics text') + }) }) diff --git a/packages/ui-color-picker/src/ColorPreset/__tests__/ColorPreset.test.tsx b/packages/ui-color-picker/src/ColorPreset/__tests__/ColorPreset.test.tsx index 3d9f65abb7..4ecc4f2a25 100644 --- a/packages/ui-color-picker/src/ColorPreset/__tests__/ColorPreset.test.tsx +++ b/packages/ui-color-picker/src/ColorPreset/__tests__/ColorPreset.test.tsx @@ -22,9 +22,10 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { userEvent } from '@testing-library/user-event' -import { vi } from 'vitest' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' +import { colorToRGB } from '@instructure/ui-color-utils' import { ColorPreset } from '@instructure/ui-color-picker/latest' import type { ColorPresetProps } from '@instructure/ui-color-picker/latest' @@ -80,8 +81,8 @@ describe('', () => { colorScreenReaderLabel: mockScreenReaderLabel } - render() - const buttons = screen.getAllByRole('button') + await render() + const buttons = page.getByRole('button').elements() buttons.forEach((button, index) => { const expectedColor = testValue.colors[index] @@ -90,8 +91,8 @@ describe('', () => { }) it('should default to using the hex code as aria-label when colorScreenReaderLabel is not provided ', async () => { - render() - const buttons = screen.getAllByRole('button') + await render() + const buttons = page.getByRole('button').elements() buttons.forEach((button, index) => { const expectedColor = testValue.colors[index] @@ -102,7 +103,7 @@ describe('', () => { describe('elementRef prop', () => { it('should provide ref', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( ) @@ -112,9 +113,9 @@ describe('', () => { describe('label prop', () => { it('should display title', async () => { - render() + await render() - const title = screen.getByText('This is a title') + const title = page.getByText('This is a title').element() expect(title).toBeInTheDocument() }) @@ -122,11 +123,12 @@ describe('', () => { describe('colors prop', () => { it('should render tooltips for all colors', async () => { - render() + await render() const testColors = testValue.colors - const indicators = screen.getAllByRole('button') - const tooltips = screen.getAllByRole('tooltip') + const indicators = page.getByRole('button').elements() + // queried directly: hidden tooltips are not in the a11y tree + const tooltips = document.querySelectorAll('[role="tooltip"]') expect(indicators.length).toBe(testColors.length) expect(tooltips.length).toBe(testColors.length) @@ -139,7 +141,7 @@ describe('', () => { }) it('should not render component when colors not provided and not modifiable', async () => { - render() + await render() const colorPreset = document.querySelector('span[class$="-colorPreset"]') @@ -150,13 +152,13 @@ describe('', () => { describe('onSelect prop', () => { it('should fire with color hex when indicator clicked', async () => { const onSelect = vi.fn() - render() + await render() - const indicators = screen.getAllByRole('button') + const indicators = page.getByRole('button').elements() await userEvent.click(indicators[1]) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).toHaveBeenCalledWith(testValue.colors[1]) }) }) @@ -164,27 +166,28 @@ describe('', () => { it('should fire with color hex when transparent indicator clicked', async () => { const testColor = '#12345678' const onSelect = vi.fn() - render( + await render( ) - const indicators = screen.getAllByRole('button') + const indicators = page.getByRole('button').elements() await userEvent.click(indicators[0]) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).toHaveBeenCalledWith(testColor) }) }) it('should not fire when disabled prop is set', async () => { const onSelect = vi.fn() - render() + await render() - const indicators = screen.getAllByRole('button') + const indicators = page.getByRole('button').elements() - await userEvent.click(indicators[1]) + // `force` because Playwright refuses to click disabled elements + await userEvent.click(indicators[1], { force: true }) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).not.toHaveBeenCalled() }) }) @@ -192,13 +195,13 @@ describe('', () => { describe('colorMixerSettings prop', () => { it('displays "new color" button', async () => { - render( + await render( ) - const buttons = screen.getAllByRole('button') + const buttons = page.getByRole('button').elements() expect(buttons[0]).toHaveTextContent( testColorMixerSettings.addNewPresetButtonScreenReaderLabel @@ -206,7 +209,7 @@ describe('', () => { }) it('renders color menus for all indicators', async () => { - render( + await render( ', () => { }) // The accessibility tests are ignored because the tooltips of the ColorIndicator, which are defined in the "aria-labelledby" attribute, are located out of the scope of the ColorPreset. + + it('should display color indicators for all colors', async () => { + await render() + + const indicators = document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + + expect(indicators.length).toBe(testValue.colors.length) + + indicators.forEach((indicator, index) => { + const expectedColor = colorToRGB(testValue.colors[index]) + const boxShadow = getComputedStyle(indicator).boxShadow + const colorValue = boxShadow.split(')')[0] + ')' + + expect(colorToRGB(colorValue)).toEqual(expectedColor) + }) + }) + + it('empty string should leave all unselected', async () => { + await render() + + expect( + document.querySelector('button[aria-label="selected"]') + ).not.toBeInTheDocument() + expect( + document.querySelector('div[class$="__selectedIndicator"]') + ).not.toBeInTheDocument() + }) + + it('should select proper color', async () => { + const testableColor = testValue.colors[6] + await render() + + const selectedButton = document + .querySelector('[class*="selectedIndicator"]')! + .closest('button')! + const indicator = selectedButton.querySelector( + 'div[role="presentation"][class$="-colorIndicator"]' + )! + const boxShadow = getComputedStyle(indicator).boxShadow + const colorValue = boxShadow.split(')')[0] + ')' + + expect(colorToRGB(colorValue)).toEqual(colorToRGB(testableColor)) + }) + + it('shows menu on indicator click', async () => { + await render( + + ) + const indicators = document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + + // an earlier test may have left the pointer over another indicator, whose + // tooltip would then cover the one we want to click + await userEvent.unhover(indicators[0]) + await userEvent.click(indicators[5]) + + await vi.waitFor(() => { + expect( + document.querySelector('div[id^=DrilldownHeader-Title]') + ).toHaveTextContent(testValue.colors[5]) + }) + + const menu = document.querySelector('div[role="menu"]')! + + expect(menu).toHaveTextContent(testColorMixerSettings!.selectColorLabel!) + expect(menu).toHaveTextContent(testColorMixerSettings!.removeColorLabel!) + }) + + it('should allow adding presets', async () => { + const onPresetChange = vi.fn() + await render( + + ) + + await userEvent.click( + document.querySelector('div[class$="addNewPresetButton"]')! + ) + + const footer = document.querySelector('div[class$="popoverFooter"]')! + const addButton = Array.from(footer.querySelectorAll('button')).find( + (button) => button.textContent?.includes('Add') + )! + + await userEvent.click(addButton) + + // Adding a preset calls onPresetChange with the new color prepended to the + // existing colors array. + await vi.waitFor(() => { + expect(onPresetChange).toHaveBeenCalledTimes(1) + expect(onPresetChange.mock.lastCall![0]).toHaveLength( + testValue.colors.length + 1 + ) + }) + }) + + it('should allow removing presets', async () => { + const onPresetChange = vi.fn() + await render( + + ) + const lastColorIndex = testValue.colors.length - 1 + const expectedColors = testValue.colors.slice(0, -1) + const indicators = document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + + await userEvent.click(indicators[lastColorIndex]) + await userEvent.click(page.getByRole('menuitem', { name: 'Remove' })) + + await vi.waitFor(() => { + expect(onPresetChange).toHaveBeenCalledWith(expectedColors) + }) + }) + + it('should allow selecting presets', async () => { + const testableIdx = 3 + const onSelect = vi.fn() + await render( + + ) + const indicators = document.querySelectorAll( + 'div[role="presentation"][class$="-colorIndicator"]' + ) + + await userEvent.click(indicators[testableIdx]) + await userEvent.click(page.getByRole('menuitem', { name: 'Select' })) + + await vi.waitFor(() => { + expect(onSelect).toHaveBeenCalledWith(testValue.colors[testableIdx]) + }) + }) }) diff --git a/vitest.config.mts b/vitest.config.mts index 301659c505..11db08eb30 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -124,6 +124,13 @@ export default defineConfig({ 'packages/ui-alerts/**', 'packages/ui-avatar/**', 'packages/ui-badge/**', + 'packages/ui-billboard/**', + 'packages/ui-breadcrumb/**', + 'packages/ui-buttons/**', + 'packages/ui-byline/**', + 'packages/ui-calendar/**', + 'packages/ui-checkbox/**', + 'packages/ui-color-picker/**', 'packages/ui-dialog/**' ], environment: 'jsdom', @@ -162,6 +169,13 @@ export default defineConfig({ 'packages/ui-alerts/**/__tests__/**/*.test.tsx', 'packages/ui-avatar/**/__tests__/**/*.test.tsx', 'packages/ui-badge/**/__tests__/**/*.test.tsx', + 'packages/ui-billboard/**/__tests__/**/*.test.tsx', + 'packages/ui-breadcrumb/**/__tests__/**/*.test.tsx', + 'packages/ui-buttons/**/__tests__/**/*.test.tsx', + 'packages/ui-byline/**/__tests__/**/*.test.tsx', + 'packages/ui-calendar/**/__tests__/**/*.test.tsx', + 'packages/ui-checkbox/**/__tests__/**/*.test.tsx', + 'packages/ui-color-picker/**/__tests__/**/*.test.tsx', 'packages/ui-dialog/**/__tests__/**/*.test.tsx' ], setupFiles: './vitest.setup.browser.ts', From 4fd1261764578b6459d014985d454db67f3efd88 Mon Sep 17 00:00:00 2001 From: Matyas Forian-Szabo Date: Wed, 29 Jul 2026 17:06:57 +0200 Subject: [PATCH 3/3] test(many): convert more component tests to vitest browser mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move ui-date-input, ui-date-time-input, ui-dom-utils, ui-drawer-layout and ui-drilldown from the jsdom `web` project to the real-browser `browser` project, and fold the matching Cypress component specs into the unit test files as `Component tests` blocks. Notable real-browser adaptations: - computed `transition` is never empty, so DrawerContent asserts on `transitionDuration` instead - Playwright refuses to click `aria-disabled`/`disabled` elements, so those tests click the inner label content or dispatch the event directly - a real click lands on the innermost label element, so `event.target` is checked with `toContainElement` - Drilldown keyboard nav needs a `cleanup()` + settle `afterEach` and an explicit re-focus after page transitions, because the Popover FocusRegion schedules an async focus return that otherwise swallows later key presses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- cypress/component/DateInput.cy.tsx | 1111 -------------- cypress/component/DateTimeInput.cy.tsx | 319 ---- cypress/component/DrawerLayout.cy.tsx | 201 --- cypress/component/DrawerTray.cy.tsx | 137 -- cypress/component/Drilldown.cy.tsx | 628 -------- cypress/component/DrilldownGroup.cy.tsx | 127 -- cypress/component/DrilldownOption.cy.tsx | 312 ---- cypress/component/DrilldownPage.cy.tsx | 118 -- cypress/component/DrilldownSeparator.cy.tsx | 46 - .../DateInput/__tests__/DateInput.test.tsx | 1350 ++++++++++++++++- .../DateInput2/__tests__/DateInput2.test.tsx | 133 +- .../__tests__/DateTimeInput.test.tsx | 665 ++++++-- .../src/__tests__/addEventListener.test.tsx | 10 +- .../__tests__/addInputModeListener.test.tsx | 13 +- .../addPositionChangeListener.test.tsx | 9 +- .../src/__tests__/findTabbable.test.tsx | 12 +- .../__tests__/getCSSStyleDeclaration.test.tsx | 3 +- .../src/__tests__/getClassList.test.tsx | 10 +- .../src/__tests__/getFontSize.test.tsx | 10 +- .../src/__tests__/getOffsetParents.test.tsx | 8 +- .../src/__tests__/getScrollParents.test.tsx | 24 +- .../src/__tests__/isCustomElement.test.tsx | 2 +- .../src/__tests__/isVisible.test.tsx | 41 +- .../__tests__/requestAnimationFrame.test.tsx | 5 +- .../src/__tests__/transformSelection.test.tsx | 1 + .../__tests__/DrawerContent.test.tsx | 31 +- .../__tests__/DrawerLayout.test.tsx | 230 ++- .../__tests__/DrawerTray.test.tsx | 142 +- .../Drilldown/__tests__/Drilldown.test.tsx | 785 ++++++++-- .../__tests__/DrilldownGroup.test.tsx | 311 ++-- .../__tests__/DrilldownOption.test.tsx | 442 +++++- .../__tests__/DrilldownPage.test.tsx | 193 ++- .../__tests__/DrilldownSeparator.test.tsx | 33 +- vitest.config.mts | 16 +- 34 files changed, 3768 insertions(+), 3710 deletions(-) delete mode 100644 cypress/component/DateInput.cy.tsx delete mode 100644 cypress/component/DateTimeInput.cy.tsx delete mode 100644 cypress/component/DrawerLayout.cy.tsx delete mode 100644 cypress/component/DrawerTray.cy.tsx delete mode 100644 cypress/component/Drilldown.cy.tsx delete mode 100644 cypress/component/DrilldownGroup.cy.tsx delete mode 100644 cypress/component/DrilldownOption.cy.tsx delete mode 100644 cypress/component/DrilldownPage.cy.tsx delete mode 100644 cypress/component/DrilldownSeparator.cy.tsx diff --git a/cypress/component/DateInput.cy.tsx b/cypress/component/DateInput.cy.tsx deleted file mode 100644 index 1376321af6..0000000000 --- a/cypress/component/DateInput.cy.tsx +++ /dev/null @@ -1,1111 +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 { useState } from 'react' -import 'cypress-real-events' - -import '../support/component' -import { DateInput, ApplyLocale } from '@instructure/ui/latest' -import { SinonSpy } from 'cypress/types/sinon' - -const TIMEZONES_DST = [ - { timezone: 'UTC', expectedDateIsoString: '2020-04-17T00:00:00.000Z' }, // Coordinated Universal Time UTC - { - timezone: 'America/New_York', - expectedDateIsoString: '2020-04-17T04:00:00.000Z' - }, // Eastern Time (US & Canada) UTC -4 (Daylight Saving Time) - { - timezone: 'America/Los_Angeles', - expectedDateIsoString: '2020-04-17T07:00:00.000Z' - }, // Pacific Time (US & Canada) UTC -7 (Daylight Saving Time) - { - timezone: 'Europe/London', - expectedDateIsoString: '2020-04-16T23:00:00.000Z' - }, // United Kingdom Time UTC +1 (Daylight Saving Time) - { - timezone: 'Europe/Paris', - expectedDateIsoString: '2020-04-16T22:00:00.000Z' - }, // Central European Time UTC +2 (Daylight Saving Time) - { timezone: 'Asia/Tokyo', expectedDateIsoString: '2020-04-16T15:00:00.000Z' }, // Japan Standard Time UTC +9 (No DST) - { - timezone: 'Australia/Sydney', - expectedDateIsoString: '2020-04-16T14:00:00.000Z' - }, // Australia Eastern Time UTC +10 (Daylight Saving Time ended in April) - { - timezone: 'Asia/Kolkata', - expectedDateIsoString: '2020-04-16T18:30:00.000Z' - }, // India Standard Time UTC +5:30 (No DST) - { - timezone: 'Africa/Johannesburg', - expectedDateIsoString: '2020-04-16T22:00:00.000Z' - }, // South Africa Standard Time UTC +2 (No DST) - { - timezone: 'Asia/Kathmandu', - expectedDateIsoString: '2020-04-16T18:15:00.000Z' - } // Nepal Standard Time UTC +5:45 (No DST) -] - -const TIMEZONES_NON_DST = [ - { timezone: 'UTC', expectedDateIsoString: '2020-02-17T00:00:00.000Z' }, // Coordinated Universal Time UTC - { - timezone: 'America/New_York', - expectedDateIsoString: '2020-02-17T05:00:00.000Z' - }, // Eastern Time (US & Canada) UTC -5 (Standard Time) - { - timezone: 'America/Los_Angeles', - expectedDateIsoString: '2020-02-17T08:00:00.000Z' - }, // Pacific Time (US & Canada) UTC -8 (Standard Time) - { - timezone: 'Europe/London', - expectedDateIsoString: '2020-02-17T00:00:00.000Z' - }, // United Kingdom Time UTC +0 (Standard Time) - { - timezone: 'Europe/Paris', - expectedDateIsoString: '2020-02-16T23:00:00.000Z' - }, // Central European Time UTC +1 (Standard Time) - { timezone: 'Asia/Tokyo', expectedDateIsoString: '2020-02-16T15:00:00.000Z' }, // Japan Standard Time UTC +9 (No DST) - { - timezone: 'Australia/Sydney', - expectedDateIsoString: '2020-02-16T13:00:00.000Z' - }, // Australia Eastern Time UTC +11 (Standard Time) - { - timezone: 'Asia/Kolkata', - expectedDateIsoString: '2020-02-16T18:30:00.000Z' - }, // India Standard Time UTC +5:30 (No DST) - { - timezone: 'Africa/Johannesburg', - expectedDateIsoString: '2020-02-16T22:00:00.000Z' - }, // South Africa Standard Time UTC +2 (No DST) - { - timezone: 'Asia/Kathmandu', - expectedDateIsoString: '2020-02-16T18:15:00.000Z' - } // Nepal Standard Time UTC +5:45 (No DST) -] - -const LOCALES = [ - { locale: 'af', textDirection: 'ltr' }, // Afrikaans - { locale: 'am', textDirection: 'ltr' }, // Amharic - { locale: 'ar-SA', textDirection: 'rtl' }, // Arabic (Saudi Arabia) - Arabic-Indic numerals - { locale: 'ar-DZ', textDirection: 'rtl' }, // Arabic (Algeria) - { locale: 'ar-EG', textDirection: 'rtl' }, // Arabic (Egypt) - { locale: 'ar-SY', textDirection: 'rtl' }, // Arabic (Syria) - { locale: 'ar-AE', textDirection: 'rtl' }, // Arabic (United Arab Emirates) - { locale: 'ar-IQ', textDirection: 'rtl' }, // Arabic (Iraq) - { locale: 'ar-PS', textDirection: 'rtl' }, // Arabic (Palestine) - { locale: 'az', textDirection: 'ltr' }, // Azerbaijani - { locale: 'be', textDirection: 'ltr' }, // Belarusian - { locale: 'bg', textDirection: 'ltr' }, // Bulgarian - { locale: 'bn-BD', textDirection: 'ltr' }, // Bengali (Bangladesh) - Bengali numerals - { locale: 'bs', textDirection: 'ltr' }, // Bosnian - { locale: 'ca', textDirection: 'ltr' }, // Catalan - { locale: 'cs', textDirection: 'ltr' }, // Czech - { locale: 'cy', textDirection: 'ltr' }, // Welsh - { locale: 'da', textDirection: 'ltr' }, // Danish - { locale: 'de-DE', textDirection: 'ltr' }, // German (Germany) - { locale: 'de-AT', textDirection: 'ltr' }, // German (Austria) - { locale: 'el', textDirection: 'ltr' }, // Greek - { locale: 'en-US', textDirection: 'ltr' }, // English (United States) - { locale: 'en-GB', textDirection: 'ltr' }, // English (United Kingdom) - { locale: 'es-ES', textDirection: 'ltr' }, // Spanish (Spain) - { locale: 'es-MX', textDirection: 'ltr' }, // Spanish (Mexico) - { locale: 'et', textDirection: 'ltr' }, // Estonian - { locale: 'fa', textDirection: 'ltr' }, // Persian - Persian numerals - { locale: 'fi', textDirection: 'ltr' }, // Finnish - { locale: 'fr-FR', textDirection: 'ltr' }, // French (France) - { locale: 'fr-CA', textDirection: 'ltr' }, // French (Canada) - { locale: 'ga', textDirection: 'ltr' }, // Irish - { locale: 'gl', textDirection: 'ltr' }, // Galician - { locale: 'gu', textDirection: 'ltr' }, // Gujarati - { locale: 'he', textDirection: 'ltr' }, // Hebrew - { locale: 'hi', textDirection: 'ltr' }, // Hindi - Devanagari numerals - { locale: 'hr', textDirection: 'ltr' }, // Croatian - { locale: 'hu', textDirection: 'ltr' }, // Hungarian - { locale: 'hy', textDirection: 'ltr' }, // Armenian - { locale: 'id', textDirection: 'ltr' }, // Indonesian - { locale: 'is', textDirection: 'ltr' }, // Icelandic - { locale: 'it-IT', textDirection: 'ltr' }, // Italian (Italy) - { locale: 'ja', textDirection: 'ltr' }, // Japanese - { locale: 'ka', textDirection: 'ltr' }, // Georgian - { locale: 'kk', textDirection: 'ltr' }, // Kazakh - { locale: 'km', textDirection: 'ltr' }, // Khmer - Khmer numerals - { locale: 'kn', textDirection: 'ltr' }, // Kannada - { locale: 'ko', textDirection: 'ltr' }, // Korean - { locale: 'lt', textDirection: 'ltr' }, // Lithuanian - { locale: 'lv', textDirection: 'ltr' }, // Latvian - { locale: 'mk', textDirection: 'ltr' }, // Macedonian - { locale: 'ml', textDirection: 'ltr' }, // Malayalam - { locale: 'mn', textDirection: 'ltr' }, // Mongolian - { locale: 'mr', textDirection: 'ltr' }, // Marathi - { locale: 'ms', textDirection: 'ltr' }, // Malay - { locale: 'mt', textDirection: 'ltr' }, // Maltese - { locale: 'nb', textDirection: 'ltr' }, // Norwegian Bokmål - { locale: 'ne', textDirection: 'ltr' }, // Nepali - { locale: 'nl', textDirection: 'ltr' }, // Dutch - { locale: 'nn', textDirection: 'ltr' }, // Norwegian Nynorsk - { locale: 'pa', textDirection: 'ltr' }, // Punjabi - { locale: 'pl', textDirection: 'ltr' }, // Polish - { locale: 'pt-PT', textDirection: 'ltr' }, // Portuguese (Portugal) - { locale: 'pt-BR', textDirection: 'ltr' }, // Portuguese (Brazil) - { locale: 'ro', textDirection: 'ltr' }, // Romanian - { locale: 'ru', textDirection: 'ltr' }, // Russian - { locale: 'si', textDirection: 'ltr' }, // Sinhala - { locale: 'sk', textDirection: 'ltr' }, // Slovak - { locale: 'sl', textDirection: 'ltr' }, // Slovenian - { locale: 'sq', textDirection: 'ltr' }, // Albanian - { locale: 'sr', textDirection: 'ltr' }, // Serbian - { locale: 'sv-SE', textDirection: 'ltr' }, // Swedish (Sweden) - { locale: 'sw', textDirection: 'ltr' }, // Swahili - { locale: 'ta', textDirection: 'ltr' }, // Tamil - { locale: 'te', textDirection: 'ltr' }, // Telugu - { locale: 'th', textDirection: 'ltr' }, // Thai - Thai numerals - { locale: 'tr', textDirection: 'ltr' }, // Turkish - { locale: 'uk', textDirection: 'ltr' }, // Ukrainian - { locale: 'ur', textDirection: 'ltr' }, // Urdu - Arabic script - { locale: 'uz', textDirection: 'ltr' }, // Uzbek - { locale: 'vi', textDirection: 'ltr' }, // Vietnamese - { locale: 'zh-CN', textDirection: 'ltr' }, // Chinese (Simplified) - { locale: 'zh-TW', textDirection: 'ltr' }, // Chinese (Traditional) - { locale: 'zu', textDirection: 'ltr' } // Zulu -] - -type DateInputExampleProps = { - initialValue?: string - timezone?: string - locale?: string - onChange?: SinonSpy - onRequestValidateDate?: SinonSpy -} - -const DateInputExample = ({ - initialValue = '', - timezone = 'UTC', - locale = 'en-GB', - onChange = cy.spy(), - onRequestValidateDate -}: DateInputExampleProps) => { - const [inputValue, setInputValue] = useState(initialValue) - - return ( - { - setInputValue(newInputValue) - onChange(_e, newInputValue, newDateString) - }} - {...(onRequestValidateDate && { onRequestValidateDate })} - /> - ) -} - -const RtlExample = (props) => { - const [inputValue, setInputValue] = useState(props.initialValue) - return ( -
- { - setInputValue(newInputValue) - props.onChange?.(_e, newInputValue, newDateString) - }} - /> -
- ) -} - -describe('', () => { - it('should have screen reader labels for weekday headers', () => { - const expectedWeekdays = [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday' - ] - cy.mount() - - cy.get('button[data-popover-trigger="true"]').click() - - cy.get('th[class*="-calendar__weekdayHeader"]').each(($header, index) => { - cy.wrap($header) - .find('span[class*="-screenReaderContent"]') - .should('have.text', expectedWeekdays[index]) - }) - }) - - it('should have screen reader labels for calendar days', () => { - cy.mount() - - // set system date to 2022 march - const testDate = new Date(2022, 2, 26) - cy.clock(testDate.getTime()) - - cy.get('button[data-popover-trigger="true"]').click() - cy.tick(1000) - - cy.get('button[class*="-calendarDay"]').each(($day) => { - cy.wrap($day) - .find('span[class*="-screenReaderContent"]') - .should('exist') - .and('not.be.empty') - }) - - cy.contains('button', '10').within(() => { - cy.get('span[class*="-screenReaderContent"]').should( - 'have.text', - '10 March 2022' - ) - }) - - cy.contains('button', '17').within(() => { - cy.get('span[class*="-screenReaderContent"]').should( - 'have.text', - '17 March 2022' - ) - }) - }) - - it('should open and close calendar properly and set value when select date from calendar', () => { - // Calendar opens on the current month, so freeze the clock to keep the - // selected-day value deterministic (otherwise it tracks the run date). - cy.clock(new Date(2024, 9, 15).getTime()) - cy.mount() - - cy.get('input').should('have.value', '') - cy.get('table').should('not.exist') - - cy.get('button[data-popover-trigger="true"]').click() - cy.tick(1000) - cy.get('table').should('exist') - - cy.contains('button', '17').click() - cy.tick(1000) - - cy.get('input').should('have.value', '17/10/2024') - cy.get('table').should('not.exist') - }) - - it('should select and highlight the correct day on Calendar when value is set', () => { - cy.mount( - - ) - - cy.get('input').should('have.value', '17/03/2022') - - cy.get('button[data-popover-trigger="true"]').click().wait(100) - - cy.get('div[class*="navigation-calendar"]') - .should('contain.text', 'March') - .and('contain.text', '2022') - - // Get day 16 background color for comparison - cy.contains('button', '16').within(() => { - cy.get('span[class$="-calendarDay__day"]') - .invoke('css', 'background-color') - .as('controlDayBgColor') - }) - - // Compare it to the highlighted day 17 - cy.contains('button', '17').within(() => { - cy.get('span[class$="-calendarDay__day"]') - .invoke('css', 'background-color') - .then((highlightedDayBgColor) => { - cy.get('@controlDayBgColor').should( - 'not.equal', - highlightedDayBgColor - ) - }) - }) - }) - - it('should call onChange with the new typed value', () => { - const newValue = '26/03/2021' - const expectedDateIsoString = new Date(Date.UTC(2021, 2, 26)).toISOString() - const onChange = cy.spy() - cy.mount( - - ) - - cy.get('input').clear().realType('26/03/2021') - cy.get('input').blur() - - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - newValue, - expectedDateIsoString - ) - }) - - it('should respect given local and timezone', () => { - const expectedFormattedValue = '17/10/2022' - const expectedDateIsoString = '2022-10-16T21:00:00.000Z' // Africa/Nairobi is GMT +3 - const onChange = cy.spy() - cy.mount( - - - - ) - - cy.get('button[data-popover-trigger="true"]').click() - - cy.get('thead th') - .eq(2) - .within(() => { - cy.get('[class*="screenReaderContent"]').should('have.text', 'mercredi') - cy.get('[aria-hidden="true"]').should('have.text', 'me') - }) - - cy.contains('button', '17').click() - - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - expectedFormattedValue, - expectedDateIsoString - ) - }) - - it('should read local and timezone information from environment context', () => { - const expectedFormattedValue = '2022. 10. 17.' - const expectedDateIsoString = '2022-10-17T00:00:00.000Z' - const onChange = cy.spy() - - cy.mount( - - - - ) - - cy.get('button[data-popover-trigger="true"]').click() - - cy.get('thead th') - .eq(2) - .within(() => { - cy.get('[class*="screenReaderContent"]').should('have.text', 'szerda') - cy.get('[aria-hidden="true"]').should('have.text', 'sze') - }) - - cy.contains('button', '17').click() - - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - expectedFormattedValue, - expectedDateIsoString - ) - }) - - describe('with various locales', () => { - const getDayInOriginalLanguage = (date, locale) => { - // Early guards for locales where Intl.DateTimeFormat can't formatting - if (locale === 'gu') return '૧૭' // Return hardcoded Gujarati numeral for 17 - if (locale === 'hi') return '१७' // Return hardcoded Hindi - Devanagari numeral for 17 - if (locale === 'km') return '១៧' // Return hardcoded Khmer numeral for 17 - if (locale === 'kn') return '೧೭' // Return hardcoded Kannada numeral for 17 - if (locale === 'ne') return '१७' // Return hardcoded Nepali numeral for 17 - if (locale === 'ta') return '௧௭' // Return hardcoded Tamil numeral for 17 - if (locale === 'ar-AE') return '١٧' // Return hardcoded Arabic-Indic numeral for 17 - - const dayString = new Intl.DateTimeFormat(locale, { - day: 'numeric', - calendar: 'gregory' - }).format(date) - - // Trim extra non-digit characters, - // but preserve the first sequence of numbers even if they are in a non-Western numeral system - return dayString.replace(/[^\p{N}]+$/u, '') - } - - const formatDate = (date, locale) => { - return new Intl.DateTimeFormat(locale, { - day: 'numeric', - month: 'numeric', - year: 'numeric', - calendar: 'gregory' - }).format(date) - } - - const normalizeWesternDigits = (dateText) => { - // Define numeral mappings for different numeral systems - const numeralMappings = { - // Arabic-Indic - '\u0660': '0', - '\u0661': '1', - '\u0662': '2', - '\u0663': '3', - '\u0664': '4', - '\u0665': '5', - '\u0666': '6', - '\u0667': '7', - '\u0668': '8', - '\u0669': '9', - // Persian - '\u06F0': '0', - '\u06F1': '1', - '\u06F2': '2', - '\u06F3': '3', - '\u06F4': '4', - '\u06F5': '5', - '\u06F6': '6', - '\u06F7': '7', - '\u06F8': '8', - '\u06F9': '9', - // Bengali - '\u09E6': '0', - '\u09E7': '1', - '\u09E8': '2', - '\u09E9': '3', - '\u09EA': '4', - '\u09EB': '5', - '\u09EC': '6', - '\u09ED': '7', - '\u09EE': '8', - '\u09EF': '9', - // Devanagari (Hindi) - '\u0966': '0', - '\u0967': '1', - '\u0968': '2', - '\u0969': '3', - '\u096A': '4', - '\u096B': '5', - '\u096C': '6', - '\u096D': '7', - '\u096E': '8', - '\u096F': '9', - // Thai - '\u0E50': '0', - '\u0E51': '1', - '\u0E52': '2', - '\u0E53': '3', - '\u0E54': '4', - '\u0E55': '5', - '\u0E56': '6', - '\u0E57': '7', - '\u0E58': '8', - '\u0E59': '9', - // Khmer - '\u17E0': '0', - '\u17E1': '1', - '\u17E2': '2', - '\u17E3': '3', - '\u17E4': '4', - '\u17E5': '5', - '\u17E6': '6', - '\u17E7': '7', - '\u17E8': '8', - '\u17E9': '9' - } - - // Return the date with western digits - return dateText.replace( - /[\u0660-\u0669\u06F0-\u06F9\u09E6-\u09EF\u0966-\u096F\u0E50-\u0E59\u17E0-\u17E9]/g, - (d) => numeralMappings[d] || d - ) - } - - const removeRtlMarkers = (dateText) => { - return dateText.replace(/\u200f/g, '') - } - - const hasRtlMarkers = (inputValue: string) => { - return inputValue.includes('‏') - } - - const transformDate = ({ date, locale, shouldRemoveRTL = true }) => { - const formatted = formatDate(date, locale) // ١٧/٣/٢٠٢٢ - const normalized = normalizeWesternDigits(formatted) // 172022/3/ RTL:(17[U+200F]/3[U+200F]/2022) - const rtlFree = removeRtlMarkers(normalized) // 17/3/2022 - - return shouldRemoveRTL ? rtlFree : normalized - } - - LOCALES.forEach(({ locale, textDirection }) => { - it(`should call onChange with the correct formatted value and ISO date string for locale: ${locale}`, () => { - const onChange = cy.spy() - // Setting the initial date ensures that the calendar opening on the desired position - const dateForSetInitial = new Date(Date.UTC(2022, 2, 26)) - const dateForExpectSelect = new Date(Date.UTC(2022, 2, 17)) // Thu, 17 Mar 2022 00:00:00 GMT - const expectedDateIsoString = dateForExpectSelect.toISOString() // '2022-03-17T00:00:00.000Z' - const expectedOnChangeValue = transformDate({ - date: dateForExpectSelect, - locale, - shouldRemoveRTL: false - }) - const expectedFormattedValue = transformDate({ - date: dateForExpectSelect, - locale - }) - const initialDate = transformDate({ date: dateForSetInitial, locale }) - const dayForSelect = getDayInOriginalLanguage( - dateForExpectSelect, - locale - ) // 17 (in local language) - - cy.mount( - - ) - - cy.get('button[data-popover-trigger="true"]').click() - - cy.get('table').should('be.visible') - - cy.contains('button', dayForSelect).should('be.enabled').click() - - // Retryable assertion instead of a fixed `.wait(500)` + one-shot - // `.then()` read: Cypress re-runs this callback until the input value - // settles after the click-driven onChange/state update. - cy.get('input').should(($input) => { - const inputValue = $input.val() as string - const inputValueRTLFree = removeRtlMarkers(inputValue) - const hasCorrectDirection = - (textDirection === 'rtl') === hasRtlMarkers(inputValue) - - expect(hasCorrectDirection, 'text direction matches locale').to.equal( - true - ) - expect(inputValueRTLFree).to.equal(expectedFormattedValue) - }) - - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - expectedOnChangeValue, - expectedDateIsoString - ) - }) - }) - }) - - it('should change separators according to locale', () => { - cy.mount() - - cy.get('input').as('input') - cy.get('@input').clear().realType('2022-03 26') - cy.get('@input').blur() - cy.get('input').should('have.value', '2022. 03. 26.') - - cy.get('@input').clear().realType('2022,03/26') - cy.get('@input').blur() - cy.get('input').should('have.value', '2022. 03. 26.') - }) - - it('should change leading zero according to locale', () => { - cy.mount() - - cy.get('input').as('input') - cy.get('@input').clear().realType('06.03.2022') - cy.get('@input').blur() - cy.get('input').should('have.value', '6/3/2022') - - cy.mount() - - cy.get('input').as('input') - cy.get('@input').clear().realType('06/3/2022') - cy.get('@input').blur() - cy.get('input').should('have.value', '6.03.2022') - - cy.mount() - - cy.get('input').as('input') - cy.get('@input').clear().realType('2022,3,6') - cy.get('@input').blur() - cy.get('input').should('have.value', '2022-03-06') - }) - - it('should dateFormat prop respect the provided local', () => { - const Example = () => { - const [value, setValue] = useState('') - - return ( - setValue(value)} - /> - ) - } - - cy.mount() - - // set system date to 2022 march - const testDate = new Date(2022, 2, 26) - cy.clock(testDate.getTime()) - - cy.get('input').should('have.value', '') - - cy.get('button[data-popover-trigger="true"]').click() - cy.tick(1000) - cy.contains('button', '17').click() - cy.tick(1000) - - cy.get('input').should('have.value', '2022. 03. 17.') - }) - - TIMEZONES_DST.forEach(({ timezone, expectedDateIsoString }) => { - it(`should apply correct timezone and daylight saving adjustments in DST period for: ${timezone}`, () => { - const onChange = cy.spy() - const initialDate = new Date(Date.UTC(2020, 3, 26)).toLocaleDateString( - 'en-GB' - ) - const expectedFormattedValue = '17/04/2020' - - cy.mount( - - ) - - cy.get('button[data-popover-trigger="true"]').click() - cy.contains('button', '17').click() - - cy.get('input').should('have.value', expectedFormattedValue) - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - expectedFormattedValue, - expectedDateIsoString - ) - }) - }) - - TIMEZONES_NON_DST.forEach(({ timezone, expectedDateIsoString }) => { - it(`should apply correct timezone and daylight saving adjustments in non-DST period for: ${timezone}`, () => { - const onChange = cy.spy() - const initialDate = new Date(Date.UTC(2020, 1, 26)).toLocaleDateString( - 'en-GB' - ) - const expectedFormattedValue = '17/02/2020' - - cy.mount( - - ) - - cy.get('button[data-popover-trigger="true"]').click() - cy.contains('button', '17').click() - - cy.get('input').should('have.value', expectedFormattedValue) - cy.wrap(onChange).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - expectedFormattedValue, - expectedDateIsoString - ) - }) - }) - - it('should set custom value through formatter callback', () => { - const customValue = 'customValue' - const date = new Date(2020, 10, 10) - - const Example = () => { - const [value, setValue] = useState('') - - return ( - date, - formatter: () => customValue - }} - onChange={(_e, value) => setValue(value)} - /> - ) - } - cy.mount() - - cy.get('input').should('have.value', '') - - cy.get('button[data-popover-trigger="true"]').click() - cy.contains('button', '17').click() - - cy.get('input').should('have.value', customValue) - }) - - it('should render year picker based on the withYearPicker prop', () => { - cy.mount( - - ) - // set system date to 2023 march - const testDate = new Date(2023, 2, 26) - cy.clock(testDate.getTime()) - - cy.get('button[data-popover-trigger="true"]').click() - cy.tick(1000) - - cy.get('input[id^="Select_"]').as('yearPicker') - - cy.get('@yearPicker').should('have.value', '2023') - - cy.get('[id^="Selectable_"][id$="-description"]').should( - 'have.text', - 'Year picker' - ) - - cy.get('@yearPicker').click() - cy.tick(1000) - - cy.get('ul[id^="Selectable_"]').should('be.visible') - cy.get('[class$="-optionItem"]').as('options') - cy.get('@options').should('have.length', 3) - cy.get('@options').eq(0).should('contain.text', '2024') - cy.get('@options').eq(1).should('contain.text', '2023') - cy.get('@options').eq(2).should('contain.text', '2022') - }) - - it('should set correct value using calendar year picker', () => { - const Example = () => { - const [value, setValue] = useState('') - - return ( - setValue(value)} - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 2022, - endYear: 2024 - }} - /> - ) - } - - cy.mount() - - // set system date to 2023 march - const testDate = new Date(2023, 2, 26) - cy.clock(testDate.getTime()) - - cy.get('input').should('have.value', '') - - cy.get('button[data-popover-trigger="true"]').click() - cy.tick(1000) - - cy.get('input[id^="Select_"]').as('yearPicker') - cy.get('@yearPicker').should('have.value', '2023') - - cy.get('@yearPicker').click() - cy.tick(1000) - - cy.get('[class$="-optionItem"]').eq(2).click() - cy.tick(1000) - - cy.get('@yearPicker').should('have.value', '2022') - - cy.contains('button', '17').click() - cy.tick(1000) - - cy.get('input').should('have.value', '17/03/2022') - }) - - it('should display correct year in year picker after date is typed into input', () => { - const Example = () => { - const [value, setValue] = useState('') - - return ( - setValue(value)} - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 2020, - endYear: 2024 - }} - /> - ) - } - - cy.mount() - - cy.get('input').should('have.value', '') - - cy.get('input').clear().realType('26/03/2021') - cy.get('input').blur() - - cy.get('input').should('have.value', '26/03/2021') - - cy.get('button[data-popover-trigger="true"]').click() - - cy.get('input[id^="Select_"]').as('yearPicker') - cy.get('@yearPicker').should('have.value', '2021') - }) - - it('should trigger onRequestValidateDate callback on date selection or blur event', () => { - const dateValidationSpy = cy.spy() - - cy.mount() - - cy.get('button[data-popover-trigger="true"]').as('calendarBtn') - cy.get('input[id^="TextInput_"]').as('input') - - cy.get('@calendarBtn').click() - cy.contains('button', '17').click() - - cy.wrap(dateValidationSpy).should('have.been.calledOnce') - - cy.get('@input').clear().realType('26/03/2020') - cy.get('@input').blur() - - cy.wrap(dateValidationSpy).should('have.been.calledTwice') - }) - - it('should pass necessary props to parser and formatter via dateFormat prop', () => { - const userDate = '26/03/2021' - const parserReturnedDate = new Date(1111, 11, 11) - - const parserSpy = cy.spy(() => parserReturnedDate) - const formatterSpy = cy.spy(() => '11/11/1111') - - const Example = () => { - const [value, setValue] = useState('') - - return ( - setValue(value)} - /> - ) - } - - cy.mount() - - cy.get('input').as('input') - cy.get('@input').clear().realType(userDate) - cy.get('@input').blur() - - cy.wrap(parserSpy).should('have.been.calledWith', userDate) - cy.wrap(formatterSpy).should('have.been.calledWith', parserReturnedDate) - }) - - it('should onRequestValidateDate prop pass necessary props to the callback when input value is not a valid date', () => { - const dateValidationSpy = cy.spy() - const newValue = 'not a date' - const expectedDateIsoString = '' - - cy.mount() - - cy.get('input').clear().realType(newValue) - cy.get('input').blur() - - cy.wrap(dateValidationSpy).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - newValue, - expectedDateIsoString - ) - }) - - it('should onRequestValidateDate prop pass necessary props to the callback when input value is a valid date', () => { - const dateValidationSpy = cy.spy() - const newValue = '26/03/2021' - const expectedDateIsoString = new Date(Date.UTC(2021, 2, 26)).toISOString() - - cy.mount() - - cy.get('input').clear().realType(newValue) - cy.get('input').blur() - - cy.wrap(dateValidationSpy).should( - 'have.been.calledWith', - Cypress.sinon.match.any, - newValue, - expectedDateIsoString - ) - }) - - const expectedPlaceholders = [ - { locale: 'hu', expectedPlaceHolder: 'YYYY. MM. DD.' }, - { locale: 'fr', expectedPlaceHolder: 'DD/MM/YYYY' }, - { locale: 'en-US', expectedPlaceHolder: 'M/D/YYYY' }, - { locale: 'ar-SA', expectedPlaceHolder: 'D‏/M‏/YYYY' } - ] - - expectedPlaceholders.forEach(({ locale, expectedPlaceHolder }) => { - it(`should set proper placeholder with locale: ${locale}`, () => { - cy.mount() - - cy.get('input[id^="TextInput_"]').should( - 'have.attr', - 'placeholder', - expectedPlaceHolder - ) - }) - }) - - it(`should set proper placeholder with dateFormat prop formatter callback`, () => { - const expectedPlaceHolder = 'YYYY*M*D' - - const Example = () => { - const [value, setValue] = useState('') - - return ( - { - return new Date(Date.UTC(1111, 11, 11)) - }, - formatter: (date) => { - const year = date.getFullYear() - const month = date.getMonth() + 1 - const day = date.getDate() - - // set placeholder according to created date structure 'YYYY*M*D' - return `${year}*${month}*${day}` - } - }} - onChange={(_e, value) => setValue(value)} - /> - ) - } - cy.mount() - - cy.get('input[id^="TextInput_"]').should( - 'have.attr', - 'placeholder', - expectedPlaceHolder - ) - }) -}) diff --git a/cypress/component/DateTimeInput.cy.tsx b/cypress/component/DateTimeInput.cy.tsx deleted file mode 100644 index c8a7054f1f..0000000000 --- a/cypress/component/DateTimeInput.cy.tsx +++ /dev/null @@ -1,319 +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 'cypress-real-events' - -import '../support/component' -import { DateTimeInput } from '@instructure/ui/latest' -import { DateTime } from '@instructure/ui-i18n' - -describe('', () => { - it('should merge defaultValue and initialTimeForNewDate and handle onChange when the user clears date input and select another one', () => { - const onChange = cy.spy() - const locale = 'en-US' - const timezone = 'US/Eastern' - const initialTimeForNewDate = '16:16' - const defaultValue = '2018-01-18T13:30' - - cy.mount( - - ) - cy.contains('date time description') - cy.contains('date-input label') - cy.contains('time-input label') - cy.contains('Thursday, January 18, 2018 1:30 PM') - - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('input[id^="Select_"]').as('timeInput') - - cy.get('@dateInput').should('have.value', '1/18/2018') - cy.get('@timeInput').should('have.value', '1:30 PM') - - cy.get('@dateInput').clear().blur() - - cy.get('@timeInput').should('have.value', '') - cy.wrap(onChange).should('have.been.called') - - cy.contains('button', 'Choose date').realClick().wait(100) - - cy.contains('button', '22').realClick().wait(100) - - cy.wrap(onChange) - .should('have.been.called') - .then((spy) => { - const selectedDateId: string = spy.lastCall.args[1] - const selectedDateValue = new Date(selectedDateId).toLocaleDateString( - 'en-US', - { - timeZone: 'US/Eastern', - calendar: 'gregory', - numberingSystem: 'latn' - } - ) - - cy.get('@dateInput').should('have.value', selectedDateValue) - cy.get('@timeInput').should('have.value', '4:16 PM') - - const lastCallDatePart = selectedDateId.split('T')[0] - - expect(lastCallDatePart).to.include('-22') - }) - }) - - it('should not fire the onDateChange event when DateInput value change is not a date change', () => { - const onChange = cy.spy() - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('@dateInput').realClick().wait(100) - cy.get('@dateInput').type('Not a date{enter}') - cy.get('@dateInput').blur() - cy.wrap(onChange).should('not.have.been.called') - }) - - it('should show an error message when setting a disabled date array', () => { - const locale = 'en-US' - const timezone = 'US/Eastern' - const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - const errorMsg = 'Disabled date selected!' - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('body').should('contain', errorMsg) - cy.get('@dateInput').clear().type(`05/18/2017`).blur() - - cy.get('body').should('not.contain', errorMsg) - }) - - it('should show an error message when setting a disabled date function', () => { - const locale = 'en-US' - const timezone = 'US/Eastern' - const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - const errorMsgText = 'Disabled date selected!' - const errorMsg = (_rawDate?: string) => errorMsgText - const checker = (toCheck: string) => { - return toCheck.includes('2017') - } - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('body').should('contain', errorMsgText) - cy.get('@dateInput').clear().type(`May 18, 2022`).blur() - - cy.get('body').should('not.contain', errorMsgText) - }) - - it('should clear TimeSelect when DateInput is cleared', () => { - const locale = 'en-US' - const timezone = 'US/Eastern' - const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - const invalidDateTimeMessage = 'invalidDateTimeMessage' - const props = { - description: 'date_time', - dateRenderLabel: 'date-input', - screenReaderLabels: { - calendarIcon: 'Open calendar', - prevMonthButton: 'Previous month', - nextMonthButton: 'Next month', - datePickerDialog: 'Date picker', - selectedLabel: 'selected' - }, - timeRenderLabel: 'time-input', - invalidDateTimeMessage: 'whoops', - locale, - timezone - } - - cy.mount() - - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('input[id^="Select_"]').as('timeInput') - - cy.get('@dateInput').should('have.value', '5/1/2017') - cy.get('@timeInput').should('have.value', '1:30 PM') - cy.get('body').should('contain', 'May 1, 2017 1:30 PM') - - // 1. clear programmatically - cy.mount() - - cy.get('@dateInput').should('have.value', '') - cy.get('@timeInput').should('have.value', '') - cy.get('body').should('not.contain', 'May 1, 2017 1:30 PM') - - // 2. clear via keyboard input - const newDateStr = '2022-03-29T19:00Z' - cy.mount() - - cy.get('@dateInput').should('have.value', '3/29/2022') - cy.get('@timeInput').should('have.value', '3:00 PM') - cy.get('body').should('contain', 'March 29, 2022 3:00 PM') - - cy.get('@dateInput').clear().blur() - - cy.get('@dateInput').should('have.value', '') - cy.get('@timeInput').should('have.value', '') - cy.get('body').should('not.contain', 'March 29, 2022 3:00 PM') - cy.get('body').should('not.contain', invalidDateTimeMessage) - }) - - it('should allow the user to enter any time value if allowNonStepInput is true', () => { - const onChange = cy.spy() - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('input[id^="Select_"]').as('timeInput') - - cy.get('@timeInput').clear().type(`7:34 PM`) - - cy.get('@dateInput').clear().type(`May 1, 2017`).blur() - - cy.wrap(onChange) - .should('have.been.called') - .then((spy) => { - const lastCallFirstArg = spy.lastCall.args[1] - - expect(lastCallFirstArg).to.equal('2017-05-01T23:34:00.000Z') - }) - }) - - it("should change value of TimeSelect to initialTimeForNewDate prop's value", () => { - const locale = 'en-US' - const timezone = 'US/Eastern' - - cy.mount( - - ) - cy.get('input[id^="TextInput_"]').as('dateInput') - cy.get('input[id^="Select_"]').as('timeInput') - - cy.get('@dateInput').clear().type(`May 1, 2017`).blur() - - cy.get('@timeInput').should('have.value', '5:05 AM') - }) -}) diff --git a/cypress/component/DrawerLayout.cy.tsx b/cypress/component/DrawerLayout.cy.tsx deleted file mode 100644 index d27d867c94..0000000000 --- a/cypress/component/DrawerLayout.cy.tsx +++ /dev/null @@ -1,201 +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 { expect } from 'chai' - -import '../support/component' -import { px, within } from '@instructure/ui-utils' -import DrawerLayoutFixture from '@instructure/ui-drawer-layout/src/DrawerLayout/v1/__fixtures__/DrawerLayout.fixture' - -describe('', () => { - it('with no overlay, layout content should have margin equal to tray width with placement=start', () => { - cy.mount( - - ) - - cy.contains('div', 'Hello from tray').then(($tray) => { - const trayWidth = px($tray.css('width')) - - cy.get('div[class$="-drawerLayout__content"]').should(($content) => { - const marginLeft = px($content.css('margin-left')) - - expect(within(marginLeft, 250, 2)).to.equal(true) - expect(marginLeft).to.equal(trayWidth) - }) - }) - }) - - it(`with no overlay, layout content should have margin equal to tray width with placement=end`, () => { - cy.mount( - - ) - - cy.contains('div', 'Hello from tray').then(($tray) => { - const trayWidth = px($tray.css('width')) - - cy.get('div[class$="-drawerLayout__content"]').should(($content) => { - const marginRight = px($content.css('margin-right')) - - expect(within(marginRight, 250, 2)).to.equal(true) - expect(marginRight).to.equal(trayWidth) - }) - }) - }) - - it(`with overlay, layout content should have a margin of zero with placement=start`, () => { - cy.mount( - - ) - - cy.get('div[class$="-drawerLayout__content"]').should(($content) => { - const marginLeft = px($content.css('margin-left')) - - expect(marginLeft).to.equal(0) - }) - }) - - it(`with overlay, layout content should have a margin of zero with placement=end`, () => { - cy.mount( - - ) - - cy.get('div[class$="-drawerLayout__content"]').should(($content) => { - const marginRight = px($content.css('margin-right')) - - expect(marginRight).to.equal(0) - }) - }) - - it('the tray should overlay the content when the content is less than the minWidth', () => { - const onOverlayTrayChange = cy.spy() - - cy.mount( - - ) - - // set prop layoutWidth - cy.mount( - - ) - - cy.wrap(onOverlayTrayChange).should('have.been.calledWith', true) - }) - - it('the tray should stop overlaying the content when there is enough space for the content', () => { - const onOverlayTrayChange = cy.spy() - - cy.mount( - - ) - - // set prop layoutWidth - cy.mount( - - ) - - cy.wrap(onOverlayTrayChange).should('have.been.calledWith', false) - }) - - it('the tray should be set to overlay when it is opened and there is not enough space', () => { - const onOverlayTrayChange = cy.spy() - - cy.mount( - - ) - - // set prop open - cy.mount( - - ) - - cy.wrap(onOverlayTrayChange).should('have.been.calledWith', true) - }) - - it('the tray should not overlay on open when there is enough space', () => { - const onOverlayTrayChange = cy.spy() - - cy.mount( - - ) - - // set prop open - cy.mount( - - ) - - cy.wrap(onOverlayTrayChange).should('have.been.calledWith', false) - }) -}) diff --git a/cypress/component/DrawerTray.cy.tsx b/cypress/component/DrawerTray.cy.tsx deleted file mode 100644 index 95cc232115..0000000000 --- a/cypress/component/DrawerTray.cy.tsx +++ /dev/null @@ -1,137 +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 '../support/component' -import canvas from '@instructure/ui-themes' -import { - DrawerLayoutContext, - DrawerTray -} from '@instructure/ui-drawer-layout/src/DrawerLayout/v1' -import { InstUISettingsProvider } from '@instructure/emotion' - -describe('', () => { - it('should render tray content when open', () => { - cy.mount( - { - return 'Hello from layout tray' - }} - /> - ) - cy.get('div[class$="-drawerTray__content"]').should( - 'have.text', - 'Hello from layout tray' - ) - }) - - it('should not render tray content when closed', () => { - cy.mount( - { - return 'Hello from layout tray' - }} - /> - ) - cy.get('div[class$="-drawerTray__content"]').should('not.exist') - }) - - it(`should place the tray correctly with placement=start`, () => { - cy.mount( - { - return 'Hello from layout tray' - }} - /> - ) - cy.get('div[class$="-drawerTray__content"]') - .parent() - .should('have.css', 'left', '0px') - }) - - it(`should place the tray correctly with placement=end`, () => { - cy.mount( - { - return 'Hello from layout tray' - }} - /> - ) - cy.get('div[class$="-drawerTray__content"]') - .parent() - .should('have.css', 'right', '0px') - }) - - it('should apply theme overrides when open', () => { - cy.mount( - { - return 'Hello from layout tray' - }} - /> - ) - cy.get('div[class$="-drawerTray__content"]') - .parent() - .should('have.css', 'z-index', '333') - }) - - it('drops a shadow if the prop is set, and it is overlaying content', () => { - const onEntered = cy.spy() - cy.mount( - - - { - return 'Hello from layout tray' - }} - /> - - - ) - cy.get('div[class*="-drawerTray--with-shadow"]').should( - 'have.css', - 'box-shadow' - ) - cy.get('div[class*="-drawerTray--with-shadow"]').should( - 'not.have.css', - 'box-shadow', - 'none' - ) - }) -}) diff --git a/cypress/component/Drilldown.cy.tsx b/cypress/component/Drilldown.cy.tsx deleted file mode 100644 index a154ed7034..0000000000 --- a/cypress/component/Drilldown.cy.tsx +++ /dev/null @@ -1,628 +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 'cypress-real-events' - -import { Drilldown } from '@instructure/ui/latest' -import '../support/component' - -const data = Array(5) - .fill(0) - .map((_v, ind) => ({ - label: `option ${ind}`, - id: `opt_${ind}` - })) - -const renderOptions = (page: string) => { - return data.map((option) => ( - - {option.label} - {page} - - )) -} - -describe('', () => { - beforeEach(() => { - cy.get('body').realMouseMove(0, 0, { position: 'bottomRight' }) - }) - - it('should disabled prop prevent option actions', () => { - cy.mount( - - - - Option-0 - - - - Option-1 - - - ) - cy.contains('Option-0').realClick() - cy.contains('Option-0').should('be.visible') - cy.contains('Option-1').should('not.exist') - }) - - it('should disabled trigger, if disabled prop provided', () => { - cy.mount( - Toggle} - > - - Option - - - ) - - cy.get('[data-test-id="toggleButton"]') - .should('have.attr', 'aria-disabled', 'true') - .and('be.disabled') - - cy.get('[data-test-id="toggleButton"]').click({ force: true }) - - cy.get('#page0option').should('not.exist') - }) - - it('should rotate focus in the drilldown by default', () => { - cy.mount( - - - Option1 - Option2 - - - ) - cy.get('div[role="menu"]').focus() - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option01') - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option02') - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option01') - }) - - it('should prevent focus rotation in the drilldown with "false"', () => { - cy.mount( - - - Option - Option - - - ) - cy.get('div[role="menu"]').focus() - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option01') - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option02') - - cy.realPress('ArrowDown') - cy.focused().should('have.id', 'option02') - }) - - it('should set the width of the drilldown', () => { - cy.mount( - - - Option - - - ) - cy.get('[role="menu"]').should('have.css', 'width', '320px') - }) - - it('should set the width of the drilldown in the popover', () => { - cy.mount( - Toggle} - defaultShow - > - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'width', - '320px' - ) - }) - - it('should be overruled by maxWidth prop', () => { - cy.mount( - - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'width', - '160px' - ) - }) - - it('should be affected by overflowX prop', () => { - cy.mount( - - - -
- Option with a very long label so that it has to break -
-
-
-
- ) - cy.get('[class$="-drilldown__container"]') - // 318px, not 320px: the container renders a 1px border each side - // (borderWidth="small") with box-sizing border-box, so the computed - // content width is 320 - 2 = 318. - .should('have.css', 'width', '318px') - .and('have.css', 'overflow-x', 'auto') - .then(($container) => { - const scrollWidth = $container[0].scrollWidth - const clientWidth = $container[0].clientWidth - - cy.wrap(scrollWidth > clientWidth).should('be.true') - }) - }) - - it('should set minWidth in popover mode', () => { - cy.mount( - Trigger} - show - onToggle={cy.spy()} - > - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'width', - '336px' - ) - }) - - it('should set the height of the drilldown', () => { - cy.mount( - - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'height', - '320px' - ) - }) - - it('should set the height of the drilldown in the popover', () => { - cy.mount( - Toggle} - defaultShow - > - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'height', - '320px' - ) - }) - - it('should be overruled by maxHeight prop', () => { - cy.mount( - - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'height', - '160px' - ) - }) - - it('should be affected by overflowY prop', () => { - cy.mount( - - - Option - Option - Option - Option - Option - Option - - - ) - cy.get('[class$="-drilldown__container"]') - .should('have.css', 'height', '160px') - .and('have.css', 'overflow-y', 'auto') - .then(($container) => { - const scrollHeight = $container[0].scrollHeight - const clientHeight = $container[0].clientHeight - - cy.wrap(scrollHeight > clientHeight).should('be.true') - }) - }) - - it('should minHeight prop set height', () => { - cy.mount( - - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'height', - '336px' - ) - }) - - it('should minHeight prop set height in popover mode', () => { - cy.mount( - Trigger} - show - onToggle={cy.spy()} - > - - Option - - - ) - cy.get('[class$="-drilldown__container"]').should( - 'have.css', - 'height', - '336px' - ) - }) - - it('should call onDismiss when Drilldown is closed', () => { - const onDismiss = cy.spy() - cy.mount( - Options} - onDismiss={onDismiss} - defaultShow - > - - Option 0 - - - ) - cy.get('div[role="menu"]').focus() - - cy.realPress('Escape') - - cy.wrap(onDismiss) - .should('have.been.called') - .and( - 'have.been.calledWithMatch', - Cypress.sinon.match.instanceOf(Event), - false - ) - }) - - it('should shouldHideOnSelect prop be true by default', () => { - cy.mount( - Toggle} - defaultShow - > - - Option-01 - - - ) - cy.get('#option01').should('exist') - cy.get('#option01').click() - cy.get('#option01').should('not.exist') - }) - - it('should not close on subPage nav, even if shouldHideOnSelect is "true"', () => { - cy.mount( - Toggle} - defaultShow - shouldHideOnSelect={true} - > - - - Option - - - - Sub-Option - - - ) - cy.get('#option01').should('exist') - cy.get('#option11').should('not.exist') - - cy.get('#option01').click() - - cy.get('#option01').should('not.exist') - cy.get('#option11').should('exist') - }) - - it('should not close on Back nav, even if shouldHideOnSelect is "true"', () => { - cy.mount( - Toggle} - defaultShow - shouldHideOnSelect={true} - > - - - Option01 - - - - Sub-Option - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Option01').should('not.exist') - cy.contains('Sub-Option').should('be.visible') - - cy.contains('Back').click() - - cy.contains('Sub-Option').should('not.exist') - cy.contains('Option01').should('be.visible') - }) - - it('should prevent closing when shouldHideOnSelect is "false"', () => { - cy.mount( - Toggle} - defaultShow - shouldHideOnSelect={false} - > - - Option01 - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Option01').should('be.visible') - }) - - it('should be able to navigate between options with up/down arrows', () => { - cy.mount( - - - {data.map((option) => ( - - {option.label} - - ))} - - - ) - cy.get('div[role="menu"]').focus() - - cy.realPress('ArrowDown') - cy.get('#opt_0').should('have.focus') - - cy.realPress('ArrowDown') - cy.get('#opt_1').should('have.focus') - - cy.realPress('ArrowDown') - cy.get('#opt_2').should('have.focus') - - cy.realPress('ArrowUp') - cy.get('#opt_1').should('have.focus') - }) - - it('should be able to navigate forward between pages with right arrow', () => { - cy.mount( - - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - cy.get('div[role="menu"]').focus() - - // the option which navigates to next page should be focused - cy.realPress('ArrowDown') - cy.get('#opt0').should('have.focus').and('have.text', 'To Page 1') - - // go to Page 1 - cy.realPress('ArrowRight') - - // on the Page 1 the 1st option is the `Back` button - cy.realPress('ArrowDown') - cy.contains('[role="menuitem"]', 'Back').should('have.focus') - - // next arrowDown should skip the header Title and focus on 'To Page 2' option - cy.realPress('ArrowDown') - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 1').should('be.visible') - cy.get('#opt5').should('have.focus').and('have.text', 'To Page 2') - - // go to Page 2 - cy.realPress('ArrowRight') - - // on Page 2 the header title should be 'Page 2' - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 2').should('be.visible') - }) - - it('should be able to navigate back to previous page with left arrow', () => { - cy.mount( - - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - cy.get('div[role="menu"]').focus() - - // go to Page 1 - cy.realPress('ArrowDown') - cy.realPress('ArrowRight') - - // on Page 1 should be visible header title - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 1').should('be.visible') - - // go to Page 0 - cy.realPress('ArrowLeft') - - // on Page 0 should be visible header title - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('be.visible') - }) - - it('should close the drilldown on root page and left arrow is pressed', () => { - cy.mount( - options} - defaultShow - > - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - cy.get('div[role="menu"]').focus() - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('be.visible') - - cy.realPress('ArrowLeft') - - cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('not.exist') - cy.contains('div[role="menu"]').should('not.exist') - }) - - it('should correctly return focus when "trigger" and "shouldReturnFocus" is set', () => { - cy.mount( - Options} - shouldReturnFocus - > - - Option-0 - - - ) - cy.contains('button', 'Options').focus() - cy.contains('Option-0').should('not.exist') - - cy.realPress('Space') - - cy.contains('Option-0').should('be.visible') - - cy.realPress('Escape') - - cy.contains('Option-0').should('not.exist') - cy.contains('button', 'Options').should('have.focus') - }) -}) diff --git a/cypress/component/DrilldownGroup.cy.tsx b/cypress/component/DrilldownGroup.cy.tsx deleted file mode 100644 index 0200468a5e..0000000000 --- a/cypress/component/DrilldownGroup.cy.tsx +++ /dev/null @@ -1,127 +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 'cypress-real-events' - -import { Drilldown } from '@instructure/ui/latest' -import '../support/component' - -function mountDrilldown(selectableType, defaultSelected) { - cy.mount( - - - - - Option0 - - - Option1 - - - Option2 - - - - - ) -} - -describe('', () => { - it('should toggle the selected option only when selectableType is multiple', () => { - const selectedValues = ['item0', 'item1', 'item2'] - const selectableType = 'multiple' - - mountDrilldown(selectableType, selectedValues) - - cy.get('[role="menuitemcheckbox"]').each(($option) => { - cy.wrap($option).should('have.attr', 'aria-checked', 'true') - }) - - cy.get('[role="menuitemcheckbox"]').eq(1).realClick() - - cy.get('[role="menuitemcheckbox"]') - .eq(0) - .should('have.attr', 'aria-checked', 'true') - cy.get('[role="menuitemcheckbox"]') - .eq(1) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitemcheckbox"]') - .eq(2) - .should('have.attr', 'aria-checked', 'true') - }) - - it('should toggle options in radio fashion when selectableType is single', () => { - const selectedValues = ['item0'] - const selectableType = 'single' - - mountDrilldown(selectableType, selectedValues) - - cy.get('[role="menuitemradio"]') - .eq(0) - .should('have.attr', 'aria-checked', 'true') - cy.get('[role="menuitemradio"]') - .eq(1) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitemradio"]') - .eq(2) - .should('have.attr', 'aria-checked', 'false') - - cy.get('[role="menuitemradio"]').eq(1).realClick() - - cy.get('[role="menuitemradio"]') - .eq(0) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitemradio"]') - .eq(1) - .should('have.attr', 'aria-checked', 'true') - cy.get('[role="menuitemradio"]') - .eq(2) - .should('have.attr', 'aria-checked', 'false') - }) - - it('should themeOverride prop passed to the Options component', () => { - cy.mount( - - - - Option - Option - - - - ) - - cy.contains('[role="presentation"]', 'Group label') - .should('exist') - .and('have.css', 'color', 'rgb(100, 0, 0)') - }) -}) diff --git a/cypress/component/DrilldownOption.cy.tsx b/cypress/component/DrilldownOption.cy.tsx deleted file mode 100644 index b57455de6c..0000000000 --- a/cypress/component/DrilldownOption.cy.tsx +++ /dev/null @@ -1,312 +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 'cypress-real-events' - -import { Drilldown } from '@instructure/ui/latest' -import '../support/component' - -describe('', () => { - it('should allow controlled behaviour', () => { - const options = ['one', 'two', 'three'] - const Example = ({ - opts, - selected - }: { - opts: typeof options - selected: string - }) => { - return ( - - - - {opts.map((opt) => { - return ( - - {opt} - - ) - })} - - - - ) - } - cy.mount() - - cy.get('[role="menuitem"]') - .eq(0) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitem"]') - .eq(1) - .should('have.attr', 'aria-checked', 'true') - cy.get('[role="menuitem"]') - .eq(2) - .should('have.attr', 'aria-checked', 'false') - - cy.mount() - - cy.get('[role="menuitem"]') - .eq(0) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitem"]') - .eq(1) - .should('have.attr', 'aria-checked', 'false') - cy.get('[role="menuitem"]') - .eq(2) - .should('have.attr', 'aria-checked', 'true') - }) - - it('should navigate to subPage on select', () => { - cy.mount( - - - - Option01 - - - - Sub-Option - - - ) - cy.contains('Sub-Option').should('not.exist') - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Sub-Option').should('be.visible') - cy.contains('Option01').should('not.exist') - }) - - it('should disabled prop apply disabled css style', () => { - cy.mount( - - - - Option - - - - ) - cy.get('#option1') - .should('have.attr', 'aria-disabled', 'true') - .and('have.css', 'cursor', 'not-allowed') - }) - - it('should navigate to url on Focus + Space', () => { - cy.mount( - - - - Option - - - - ) - cy.get('#option1').focus().realPress('Space') - - cy.url().should('include', '#helloWorld') - }) - - it('should navigate to url on Click', () => { - cy.mount( - - - - Option - - - - ) - cy.get('#option1').realClick() - cy.url().should('include', '#helloWorld') - }) - - it("shouldn't navigate to url, if disabled", () => { - cy.mount( - - - - Option - - - - ) - // Prior tests navigate to `#helloWorld` and the URL hash persists across - // tests; clear it so this test truly verifies the disabled option does not - // navigate (rather than passing/failing on leftover state). - cy.window().then((win) => { - const { location } = win - location.hash = '' - }) - - cy.get('#option1').realClick() - - cy.url().should('not.include', '#helloWorld') - }) - - it('should renderLabelInfo prop affected by afterLabelContentVAlign prop', () => { - cy.mount( - - - - Option - - - - ) - cy.contains('[class$=-drilldown__optionLabelInfo]', 'Info').should( - 'have.css', - 'align-self', - 'flex-end' - ) - }) - - it('should provide goToPreviousPage method that goes back to the previous page', () => { - cy.mount( - - - - Option01 - - - - - { - goToPreviousPage() - }} - > - Option11 - - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Option01').should('not.exist') - cy.contains('Option11').should('be.visible') - - cy.get('#option11').click() - - cy.contains('Option01').should('be.visible') - cy.contains('Option11').should('not.exist') - }) - - it('should provide goToPage method that can be used to go back a page', () => { - cy.mount( - - - - Option01 - - - - - { - goToPage(pageHistory[0]) - }} - > - Option11 - - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Option01').should('not.exist') - cy.contains('Option11').should('be.visible') - - cy.get('#option11').click() - - cy.contains('Option01').should('be.visible') - cy.contains('Option11').should('not.exist') - }) - - it('should provide goToPage method that can be used to go to a new, existing page', () => { - cy.mount( - - - { - goToPage('page1') - }} - > - Option01 - - - - - Option11 - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option01').click() - - cy.contains('Option01').should('not.exist') - cy.contains('Option11').should('be.visible') - }) - - it('should themeOverride prop passed to the Options.Item component', () => { - cy.mount( - - - - Option01 - - - - ) - cy.contains('li[class$="-optionItem"]', 'Option01') - .should('have.css', 'color', 'rgb(0, 0, 100)') - .and('have.css', 'backgroundColor', 'rgb(200, 200, 200)') - }) -}) diff --git a/cypress/component/DrilldownPage.cy.tsx b/cypress/component/DrilldownPage.cy.tsx deleted file mode 100644 index 62f6e5a0fe..0000000000 --- a/cypress/component/DrilldownPage.cy.tsx +++ /dev/null @@ -1,118 +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 'cypress-real-events' - -import { Drilldown } from '@instructure/ui/latest' -import '../support/component' - -describe('', () => { - it('should have a back arrow in header back navigation', () => { - cy.mount( - - - - Option01 - - - - Option22 - - - ) - - cy.get('#option1').click() - - cy.contains('li[class$="-optionItem"]', 'HeaderBackString') - // v2 renders the back button with a lucide ChevronLeft (was IconArrowOpenStart) - .find('svg[name="ChevronLeft"]') - .should('exist') - }) - - it('should still display the back icon in header back navigation, even if function has no return value', () => { - cy.mount( - - - - Option01 - - - null}> - Option22 - - - ) - cy.get('#option1').click() - - cy.get('div[role="menu"]') - // v2 renders the back button with a lucide ChevronLeft (was IconArrowOpenStart) - .find('svg[name="ChevronLeft"]') - .should('exist') - }) - - it('should fire onBackButtonClicked on header back navigation click', () => { - const backNavCallback = cy.spy() - cy.mount( - - - - Option01 - - - - Option22 - - - ) - cy.get('#option1').click() - - cy.contains('li[class$="-optionItem"]', 'Back').click() - - cy.wrap(backNavCallback).should('have.been.called') - }) - - it('should go back one page on click', () => { - cy.mount( - - - - Option01 - - - - Option22 - - - ) - cy.contains('Option01').should('be.visible') - - cy.get('#option1').click() - - cy.contains('Option01').should('not.exist') - - cy.contains('li[class$="-optionItem"]', 'Back').click() - - cy.contains('Option01').should('be.visible') - }) -}) diff --git a/cypress/component/DrilldownSeparator.cy.tsx b/cypress/component/DrilldownSeparator.cy.tsx deleted file mode 100644 index ff0a24f00c..0000000000 --- a/cypress/component/DrilldownSeparator.cy.tsx +++ /dev/null @@ -1,46 +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 'cypress-real-events' - -import { Drilldown } from '@instructure/ui/latest' -import '../support/component' - -describe('', () => { - it('themeOverride prop should pass overrides to Option.Separator', () => { - cy.mount( - - - - - - ) - cy.get('#separator1') - .should('have.css', 'height', '16px') - .and('have.css', 'backgroundColor', 'rgb(0, 128, 0)') - }) -}) diff --git a/packages/ui-date-input/src/DateInput/__tests__/DateInput.test.tsx b/packages/ui-date-input/src/DateInput/__tests__/DateInput.test.tsx index 94c65a3cde..6706373d74 100644 --- a/packages/ui-date-input/src/DateInput/__tests__/DateInput.test.tsx +++ b/packages/ui-date-input/src/DateInput/__tests__/DateInput.test.tsx @@ -22,11 +22,20 @@ * SOFTWARE. */ import { useState } from 'react' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { vi, MockInstance } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, + MockInstance +} from 'vitest' import { HeartInstUIIcon } from '@instructure/ui-icons' +import { ApplyLocale } from '@instructure/ui-i18n' import { DateInput } from '@instructure/ui-date-input/latest' import { TextInput } from '@instructure/ui-text-input/latest' @@ -54,6 +63,260 @@ const DateInputExample = () => { ) } +const TIMEZONES_DST = [ + { timezone: 'UTC', expectedDateIsoString: '2020-04-17T00:00:00.000Z' }, // Coordinated Universal Time UTC + { + timezone: 'America/New_York', + expectedDateIsoString: '2020-04-17T04:00:00.000Z' + }, // Eastern Time (US & Canada) UTC -4 (Daylight Saving Time) + { + timezone: 'America/Los_Angeles', + expectedDateIsoString: '2020-04-17T07:00:00.000Z' + }, // Pacific Time (US & Canada) UTC -7 (Daylight Saving Time) + { + timezone: 'Europe/London', + expectedDateIsoString: '2020-04-16T23:00:00.000Z' + }, // United Kingdom Time UTC +1 (Daylight Saving Time) + { + timezone: 'Europe/Paris', + expectedDateIsoString: '2020-04-16T22:00:00.000Z' + }, // Central European Time UTC +2 (Daylight Saving Time) + { timezone: 'Asia/Tokyo', expectedDateIsoString: '2020-04-16T15:00:00.000Z' }, // Japan Standard Time UTC +9 (No DST) + { + timezone: 'Australia/Sydney', + expectedDateIsoString: '2020-04-16T14:00:00.000Z' + }, // Australia Eastern Time UTC +10 (Daylight Saving Time ended in April) + { + timezone: 'Asia/Kolkata', + expectedDateIsoString: '2020-04-16T18:30:00.000Z' + }, // India Standard Time UTC +5:30 (No DST) + { + timezone: 'Africa/Johannesburg', + expectedDateIsoString: '2020-04-16T22:00:00.000Z' + }, // South Africa Standard Time UTC +2 (No DST) + { + timezone: 'Asia/Kathmandu', + expectedDateIsoString: '2020-04-16T18:15:00.000Z' + } // Nepal Standard Time UTC +5:45 (No DST) +] + +const TIMEZONES_NON_DST = [ + { timezone: 'UTC', expectedDateIsoString: '2020-02-17T00:00:00.000Z' }, // Coordinated Universal Time UTC + { + timezone: 'America/New_York', + expectedDateIsoString: '2020-02-17T05:00:00.000Z' + }, // Eastern Time (US & Canada) UTC -5 (Standard Time) + { + timezone: 'America/Los_Angeles', + expectedDateIsoString: '2020-02-17T08:00:00.000Z' + }, // Pacific Time (US & Canada) UTC -8 (Standard Time) + { + timezone: 'Europe/London', + expectedDateIsoString: '2020-02-17T00:00:00.000Z' + }, // United Kingdom Time UTC +0 (Standard Time) + { + timezone: 'Europe/Paris', + expectedDateIsoString: '2020-02-16T23:00:00.000Z' + }, // Central European Time UTC +1 (Standard Time) + { timezone: 'Asia/Tokyo', expectedDateIsoString: '2020-02-16T15:00:00.000Z' }, // Japan Standard Time UTC +9 (No DST) + { + timezone: 'Australia/Sydney', + expectedDateIsoString: '2020-02-16T13:00:00.000Z' + }, // Australia Eastern Time UTC +11 (Standard Time) + { + timezone: 'Asia/Kolkata', + expectedDateIsoString: '2020-02-16T18:30:00.000Z' + }, // India Standard Time UTC +5:30 (No DST) + { + timezone: 'Africa/Johannesburg', + expectedDateIsoString: '2020-02-16T22:00:00.000Z' + }, // South Africa Standard Time UTC +2 (No DST) + { + timezone: 'Asia/Kathmandu', + expectedDateIsoString: '2020-02-16T18:15:00.000Z' + } // Nepal Standard Time UTC +5:45 (No DST) +] + +const LOCALES = [ + { locale: 'af', textDirection: 'ltr' }, // Afrikaans + { locale: 'am', textDirection: 'ltr' }, // Amharic + { locale: 'ar-SA', textDirection: 'rtl' }, // Arabic (Saudi Arabia) - Arabic-Indic numerals + { locale: 'ar-DZ', textDirection: 'rtl' }, // Arabic (Algeria) + { locale: 'ar-EG', textDirection: 'rtl' }, // Arabic (Egypt) + { locale: 'ar-SY', textDirection: 'rtl' }, // Arabic (Syria) + { locale: 'ar-AE', textDirection: 'rtl' }, // Arabic (United Arab Emirates) + { locale: 'ar-IQ', textDirection: 'rtl' }, // Arabic (Iraq) + { locale: 'ar-PS', textDirection: 'rtl' }, // Arabic (Palestine) + { locale: 'az', textDirection: 'ltr' }, // Azerbaijani + { locale: 'be', textDirection: 'ltr' }, // Belarusian + { locale: 'bg', textDirection: 'ltr' }, // Bulgarian + { locale: 'bn-BD', textDirection: 'ltr' }, // Bengali (Bangladesh) - Bengali numerals + { locale: 'bs', textDirection: 'ltr' }, // Bosnian + { locale: 'ca', textDirection: 'ltr' }, // Catalan + { locale: 'cs', textDirection: 'ltr' }, // Czech + { locale: 'cy', textDirection: 'ltr' }, // Welsh + { locale: 'da', textDirection: 'ltr' }, // Danish + { locale: 'de-DE', textDirection: 'ltr' }, // German (Germany) + { locale: 'de-AT', textDirection: 'ltr' }, // German (Austria) + { locale: 'el', textDirection: 'ltr' }, // Greek + { locale: 'en-US', textDirection: 'ltr' }, // English (United States) + { locale: 'en-GB', textDirection: 'ltr' }, // English (United Kingdom) + { locale: 'es-ES', textDirection: 'ltr' }, // Spanish (Spain) + { locale: 'es-MX', textDirection: 'ltr' }, // Spanish (Mexico) + { locale: 'et', textDirection: 'ltr' }, // Estonian + { locale: 'fa', textDirection: 'ltr' }, // Persian - Persian numerals + { locale: 'fi', textDirection: 'ltr' }, // Finnish + { locale: 'fr-FR', textDirection: 'ltr' }, // French (France) + { locale: 'fr-CA', textDirection: 'ltr' }, // French (Canada) + { locale: 'ga', textDirection: 'ltr' }, // Irish + { locale: 'gl', textDirection: 'ltr' }, // Galician + { locale: 'gu', textDirection: 'ltr' }, // Gujarati + { locale: 'he', textDirection: 'ltr' }, // Hebrew + { locale: 'hi', textDirection: 'ltr' }, // Hindi - Devanagari numerals + { locale: 'hr', textDirection: 'ltr' }, // Croatian + { locale: 'hu', textDirection: 'ltr' }, // Hungarian + { locale: 'hy', textDirection: 'ltr' }, // Armenian + { locale: 'id', textDirection: 'ltr' }, // Indonesian + { locale: 'is', textDirection: 'ltr' }, // Icelandic + { locale: 'it-IT', textDirection: 'ltr' }, // Italian (Italy) + { locale: 'ja', textDirection: 'ltr' }, // Japanese + { locale: 'ka', textDirection: 'ltr' }, // Georgian + { locale: 'kk', textDirection: 'ltr' }, // Kazakh + { locale: 'km', textDirection: 'ltr' }, // Khmer - Khmer numerals + { locale: 'kn', textDirection: 'ltr' }, // Kannada + { locale: 'ko', textDirection: 'ltr' }, // Korean + { locale: 'lt', textDirection: 'ltr' }, // Lithuanian + { locale: 'lv', textDirection: 'ltr' }, // Latvian + { locale: 'mk', textDirection: 'ltr' }, // Macedonian + { locale: 'ml', textDirection: 'ltr' }, // Malayalam + { locale: 'mn', textDirection: 'ltr' }, // Mongolian + { locale: 'mr', textDirection: 'ltr' }, // Marathi + { locale: 'ms', textDirection: 'ltr' }, // Malay + { locale: 'mt', textDirection: 'ltr' }, // Maltese + { locale: 'nb', textDirection: 'ltr' }, // Norwegian Bokmal + { locale: 'ne', textDirection: 'ltr' }, // Nepali + { locale: 'nl', textDirection: 'ltr' }, // Dutch + { locale: 'nn', textDirection: 'ltr' }, // Norwegian Nynorsk + { locale: 'pa', textDirection: 'ltr' }, // Punjabi + { locale: 'pl', textDirection: 'ltr' }, // Polish + { locale: 'pt-PT', textDirection: 'ltr' }, // Portuguese (Portugal) + { locale: 'pt-BR', textDirection: 'ltr' }, // Portuguese (Brazil) + { locale: 'ro', textDirection: 'ltr' }, // Romanian + { locale: 'ru', textDirection: 'ltr' }, // Russian + { locale: 'si', textDirection: 'ltr' }, // Sinhala + { locale: 'sk', textDirection: 'ltr' }, // Slovak + { locale: 'sl', textDirection: 'ltr' }, // Slovenian + { locale: 'sq', textDirection: 'ltr' }, // Albanian + { locale: 'sr', textDirection: 'ltr' }, // Serbian + { locale: 'sv-SE', textDirection: 'ltr' }, // Swedish (Sweden) + { locale: 'sw', textDirection: 'ltr' }, // Swahili + { locale: 'ta', textDirection: 'ltr' }, // Tamil + { locale: 'te', textDirection: 'ltr' }, // Telugu + { locale: 'th', textDirection: 'ltr' }, // Thai - Thai numerals + { locale: 'tr', textDirection: 'ltr' }, // Turkish + { locale: 'uk', textDirection: 'ltr' }, // Ukrainian + { locale: 'ur', textDirection: 'ltr' }, // Urdu - Arabic script + { locale: 'uz', textDirection: 'ltr' }, // Uzbek + { locale: 'vi', textDirection: 'ltr' }, // Vietnamese + { locale: 'zh-CN', textDirection: 'ltr' }, // Chinese (Simplified) + { locale: 'zh-TW', textDirection: 'ltr' }, // Chinese (Traditional) + { locale: 'zu', textDirection: 'ltr' } // Zulu +] + +type ConfigurableDateInputExampleProps = { + initialValue?: string + timezone?: string + locale?: string + onChange?: (...args: any[]) => void + onRequestValidateDate?: (...args: any[]) => void +} + +const ConfigurableDateInputExample = ({ + initialValue = '', + timezone = 'UTC', + locale = 'en-GB', + onChange = vi.fn(), + onRequestValidateDate +}: ConfigurableDateInputExampleProps) => { + const [inputValue, setInputValue] = useState(initialValue) + + return ( + { + setInputValue(newInputValue) + onChange(_e, newInputValue, newDateString) + }} + {...(onRequestValidateDate && { onRequestValidateDate })} + /> + ) +} + +type RtlExampleProps = { + initialValue?: string + textDirection?: string + locale?: string + onChange?: (...args: any[]) => void +} + +const RtlExample = (props: RtlExampleProps) => { + const [inputValue, setInputValue] = useState(props.initialValue) + return ( +
+ { + setInputValue(newInputValue) + props.onChange?.(_e, newInputValue, newDateString) + }} + /> +
+ ) +} + +// the `input[id^="TextInput_"]` selector keeps this unambiguous when the +// year picker's own is also on the page +const dateInputElement = () => + document.querySelector('input[id^="TextInput_"]')! + +const openCalendar = async () => { + await userEvent.click( + document.querySelector('button[data-popover-trigger="true"]')! + ) + await vi.waitFor(() => { + expect(document.querySelector('table')).toBeInTheDocument() + }) +} + +// the day number is rendered twice (visible + screen reader label), so match +// on the button's text the same way the Cypress `cy.contains` did +const dayButton = (day: string) => + Array.from( + document.querySelectorAll( + 'button[class*="-calendarDay"]' + ) + ).find((button) => button.textContent?.includes(day))! + describe('', () => { let consoleWarningMock: MockInstance let consoleErrorMock: MockInstance @@ -67,10 +330,11 @@ describe('', () => { afterEach(() => { consoleWarningMock.mockRestore() consoleErrorMock.mockRestore() + vi.useRealTimers() }) it('should render an input', async () => { - const { container } = render() + const { container } = await render() const dateInput = container.querySelector('input') expect(dateInput).toBeInTheDocument() @@ -78,7 +342,7 @@ describe('', () => { }) it('should render an input label', async () => { - const { container } = render() + const { container } = await render() const label = container.querySelector('label') @@ -88,7 +352,7 @@ describe('', () => { it('should render an input placeholder', async () => { const placeholder = 'Placeholder' - render( + await render( ', () => { value="" /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() expect(dateInput).toHaveAttribute('placeholder', placeholder) }) it('should render a calendar icon with screen reader label', async () => { const iconLabel = 'Calendar icon Label' - const { container } = render( + const { container } = await render( ', () => { /> ) const calendarIcon = container.querySelector('svg[name="Calendar"]') - const calendarLabel = screen.getByText(iconLabel) + const calendarLabel = page.getByText(iconLabel).element() expect(calendarIcon).toBeInTheDocument() expect(calendarLabel).toBeInTheDocument() @@ -132,7 +396,7 @@ describe('', () => { it('refs should return the underlying component', async () => { const inputRef = vi.fn() const ref: React.Ref = { current: null } - const { container } = render( + const { container } = await render( ', () => { it('should render a custom calendar icon with screen reader label', async () => { const iconLabel = 'Calendar icon Label' - const { container } = render( + const { container } = await render( ', () => { /> ) const calendarIcon = container.querySelector('svg[name="Heart"]') - const calendarLabel = screen.getByText(iconLabel) + const calendarLabel = page.getByText(iconLabel).element() expect(calendarIcon).toBeInTheDocument() expect(calendarLabel).toBeInTheDocument() }) it('should not show calendar table by default', async () => { - render() - const calendarTable = screen.queryByRole('table') + await render() + const calendarTable = page.getByRole('table').query() expect(calendarTable).not.toBeInTheDocument() }) it('should show calendar table when calendar button is clicked', async () => { - render() - const calendarButton = screen.getByRole('button') + await render() + const calendarButton = page.getByRole('button').element() expect(calendarButton).toBeInTheDocument() await userEvent.click(calendarButton) - await waitFor(() => { - const calendarTable = screen.queryByRole('table') + await vi.waitFor(() => { + const calendarTable = page.getByRole('table').query() expect(calendarTable).toBeInTheDocument() }) }) @@ -201,7 +465,7 @@ describe('', () => { const nextMonthLabel = 'Next month' const prevMonthLabel = 'Previous month' - render( + await render( ', () => { value="" /> ) - const calendarButton = screen.getByRole('button') + const calendarButton = page.getByRole('button').element() await userEvent.click(calendarButton) - await waitFor(() => { - const prevMonthButton = screen.getByRole('button', { - name: new RegExp(`^${prevMonthLabel}`) - }) - const nextMonthButton = screen.getByRole('button', { - name: new RegExp(`^${nextMonthLabel}`) - }) + await vi.waitFor(() => { + const prevMonthButton = page + .getByRole('button', { + name: new RegExp(`^${prevMonthLabel}`) + }) + .element() + const nextMonthButton = page + .getByRole('button', { + name: new RegExp(`^${nextMonthLabel}`) + }) + .element() expect(prevMonthButton).toBeInTheDocument() expect(nextMonthButton).toBeInTheDocument() @@ -243,7 +511,7 @@ describe('', () => { it('should programmatically set and render the initial value', async () => { const value = '26/03/2024' - render( + await render( ', () => { value={value} /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() expect(dateInput).toHaveValue(value) expect(dateInput).toBeInTheDocument() @@ -266,7 +534,7 @@ describe('', () => { it('should set interaction type to disabled', async () => { const interactionDisabled = 'disabled' - const { container } = render( + const { container } = await render( ', () => { it('should set interaction type to readonly', async () => { const interactionReadOnly = 'readonly' - const { container } = render( + const { container } = await render( ', () => { /> ) const dateInput = container.querySelector('input') - const calendarButton = screen.getByRole('button') + const calendarButton = page.getByRole('button').element() expect(dateInput).toHaveAttribute(interactionReadOnly) expect(calendarButton).toBeInTheDocument() - await userEvent.click(calendarButton) + // `force` because Playwright refuses to click the disabled trigger + await userEvent.click(calendarButton, { force: true }) - await waitFor(() => { - const calendarTable = screen.queryByRole('table') + await vi.waitFor(() => { + const calendarTable = page.getByRole('table').query() expect(calendarTable).not.toBeInTheDocument() }) }) it('should set required', async () => { - const { container } = render( + const { container } = await render( ', () => { it('should call onBlur', async () => { const onBlur = vi.fn() - render( + await render( ', () => { onBlur={onBlur} /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(onBlur).toHaveBeenCalled() }) }) @@ -385,19 +654,19 @@ describe('', () => { ) } - render() + await render() - expect(screen.queryByText(errorMsg)).not.toBeInTheDocument() + expect(page.getByText(errorMsg).query()).not.toBeInTheDocument() - const dateInput = screen.getByLabelText(LABEL_TEXT) + const dateInput = page.getByLabelText(LABEL_TEXT).element() await userEvent.click(dateInput) await userEvent.type(dateInput, 'Not a date') dateInput.blur() - await waitFor(() => { - expect(screen.getByText(errorMsg)).toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText(errorMsg).element()).toBeInTheDocument() }) }) @@ -410,7 +679,7 @@ describe('', () => { { type: 'screenreader-only', text: 'Screenreader' } ] - render( + await render( ', () => { /> ) - expect(screen.getByText('TypeLess')).toBeVisible() - expect(screen.getByText('Error')).toBeVisible() - expect(screen.getByText('Success')).toBeVisible() - expect(screen.getByText('Hint')).toBeVisible() + expect(page.getByText('TypeLess').element()).toBeVisible() + expect(page.getByText('Error').element()).toBeVisible() + expect(page.getByText('Success').element()).toBeVisible() + expect(page.getByText('Hint').element()).toBeVisible() - const screenreaderMessage = screen.getByText('Screenreader') + const screenreaderMessage = page.getByText('Screenreader').element() expect(screenreaderMessage).toBeInTheDocument() expect(screenreaderMessage).toHaveClass(/screenReaderContent/) }) @@ -438,7 +707,7 @@ describe('', () => { it('should render date picker dialog with proper role and ARIA label', async () => { const datePickerLabel = 'Date picker' - render( + await render( ', () => { /> ) - const calendarButton = screen.getByRole('button', { name: 'Calendar' }) + const calendarButton = page + .getByRole('button', { name: 'Calendar' }) + .element() await userEvent.click(calendarButton) - await waitFor(() => { - const dialog = screen.getByRole('dialog', { name: datePickerLabel }) + await vi.waitFor(() => { + const dialog = page + .getByRole('dialog', { name: datePickerLabel }) + .element() expect(dialog).toBeInTheDocument() expect(dialog).toHaveAttribute('aria-label', datePickerLabel) }) }) + + // The tests below were ported from the DateInput Cypress spec + + it('should have screen reader labels for weekday headers', async () => { + const expectedWeekdays = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday' + ] + await render() + + await openCalendar() + + const headers = document.querySelectorAll( + 'th[class*="-calendar__weekdayHeader"]' + ) + + expect(headers).toHaveLength(expectedWeekdays.length) + headers.forEach((header, index) => { + expect( + header.querySelector('span[class*="-screenReaderContent"]')!.textContent + ).toBe(expectedWeekdays[index]) + }) + }) + + it('should have screen reader labels for calendar days', async () => { + // set system date to 2022 march + vi.useFakeTimers({ shouldAdvanceTime: true }) + vi.setSystemTime(new Date(2022, 2, 26)) + + await render() + + await openCalendar() + + const days = document.querySelectorAll('button[class*="-calendarDay"]') + + expect(days.length).toBeGreaterThan(0) + days.forEach((day) => { + const label = day.querySelector('span[class*="-screenReaderContent"]') + + expect(label).toBeInTheDocument() + expect(label).not.toBeEmptyDOMElement() + }) + + expect( + dayButton('10').querySelector('span[class*="-screenReaderContent"]')! + .textContent + ).toBe('10 March 2022') + expect( + dayButton('17').querySelector('span[class*="-screenReaderContent"]')! + .textContent + ).toBe('17 March 2022') + }) + + it('should open and close calendar properly and set value when select date from calendar', async () => { + // Calendar opens on the current month, so freeze the clock to keep the + // selected-day value deterministic (otherwise it tracks the run date). + vi.useFakeTimers({ shouldAdvanceTime: true }) + vi.setSystemTime(new Date(2024, 9, 15)) + + await render() + + expect(dateInputElement()).toHaveValue('') + expect(document.querySelector('table')).not.toBeInTheDocument() + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue('17/10/2024') + expect(document.querySelector('table')).not.toBeInTheDocument() + }) + }) + + it('should select and highlight the correct day on Calendar when value is set', async () => { + await render( + + ) + + expect(dateInputElement()).toHaveValue('17/03/2022') + + await openCalendar() + + const navigation = document.querySelector( + 'div[class*="navigation-calendar"]' + ) + + expect(navigation).toHaveTextContent('March') + expect(navigation).toHaveTextContent('2022') + + // Get day 16 background color for comparison + const controlDayBgColor = getComputedStyle( + dayButton('16').querySelector('span[class$="-calendarDay__day"]')! + ).backgroundColor + + // Compare it to the highlighted day 17 + const highlightedDayBgColor = getComputedStyle( + dayButton('17').querySelector('span[class$="-calendarDay__day"]')! + ).backgroundColor + + expect(controlDayBgColor).not.toEqual(highlightedDayBgColor) + }) + + it('should call onChange with the new typed value', async () => { + const newValue = '26/03/2021' + const expectedDateIsoString = new Date(Date.UTC(2021, 2, 26)).toISOString() + const onChange = vi.fn() + await render( + + ) + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, newValue) + dateInput.blur() + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + newValue, + expectedDateIsoString + ) + }) + }) + + it('should respect given local and timezone', async () => { + const expectedFormattedValue = '17/10/2022' + const expectedDateIsoString = '2022-10-16T21:00:00.000Z' // Africa/Nairobi is GMT +3 + const onChange = vi.fn() + await render( + + + + ) + + await openCalendar() + + const weekdayHeader = document.querySelectorAll('thead th')[2] + + expect( + weekdayHeader.querySelector('[class*="screenReaderContent"]')!.textContent + ).toBe('mercredi') + expect( + weekdayHeader.querySelector('[aria-hidden="true"]')!.textContent + ).toBe('me') + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + expectedFormattedValue, + expectedDateIsoString + ) + }) + }) + + it('should read local and timezone information from environment context', async () => { + const expectedFormattedValue = '2022. 10. 17.' + const expectedDateIsoString = '2022-10-17T00:00:00.000Z' + const onChange = vi.fn() + + await render( + + + + ) + + await openCalendar() + + const weekdayHeader = document.querySelectorAll('thead th')[2] + + expect( + weekdayHeader.querySelector('[class*="screenReaderContent"]')!.textContent + ).toBe('szerda') + expect( + weekdayHeader.querySelector('[aria-hidden="true"]')!.textContent + ).toBe('sze') + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + expectedFormattedValue, + expectedDateIsoString + ) + }) + }) + + describe('with various locales', () => { + const getDayInOriginalLanguage = (date: Date, locale: string) => { + // Early guards for locales where Intl.DateTimeFormat can't formatting + if (locale === 'gu') return '૧૭' // Return hardcoded Gujarati numeral for 17 + if (locale === 'hi') return '१७' // Return hardcoded Hindi - Devanagari numeral for 17 + if (locale === 'km') return '១៧' // Return hardcoded Khmer numeral for 17 + if (locale === 'kn') return '೧೭' // Return hardcoded Kannada numeral for 17 + if (locale === 'ne') return '१७' // Return hardcoded Nepali numeral for 17 + if (locale === 'ta') return '௧௭' // Return hardcoded Tamil numeral for 17 + if (locale === 'ar-AE') return '١٧' // Return hardcoded Arabic-Indic numeral for 17 + + const dayString = new Intl.DateTimeFormat(locale, { + day: 'numeric', + calendar: 'gregory' + }).format(date) + + // Trim extra non-digit characters, + // but preserve the first sequence of numbers even if they are in a non-Western numeral system + return dayString.replace(/[^\p{N}]+$/u, '') + } + + const formatDate = (date: Date, locale: string) => { + return new Intl.DateTimeFormat(locale, { + day: 'numeric', + month: 'numeric', + year: 'numeric', + calendar: 'gregory' + }).format(date) + } + + const normalizeWesternDigits = (dateText: string) => { + // Define numeral mappings for different numeral systems + const numeralMappings: Record = { + // Arabic-Indic + '٠': '0', + '١': '1', + '٢': '2', + '٣': '3', + '٤': '4', + '٥': '5', + '٦': '6', + '٧': '7', + '٨': '8', + '٩': '9', + // Persian + '۰': '0', + '۱': '1', + '۲': '2', + '۳': '3', + '۴': '4', + '۵': '5', + '۶': '6', + '۷': '7', + '۸': '8', + '۹': '9', + // Bengali + '০': '0', + '১': '1', + '২': '2', + '৩': '3', + '৪': '4', + '৫': '5', + '৬': '6', + '৭': '7', + '৮': '8', + '৯': '9', + // Devanagari (Hindi) + '०': '0', + '१': '1', + '२': '2', + '३': '3', + '४': '4', + '५': '5', + '६': '6', + '७': '7', + '८': '8', + '९': '9', + // Thai + '๐': '0', + '๑': '1', + '๒': '2', + '๓': '3', + '๔': '4', + '๕': '5', + '๖': '6', + '๗': '7', + '๘': '8', + '๙': '9', + // Khmer + '០': '0', + '១': '1', + '២': '2', + '៣': '3', + '៤': '4', + '៥': '5', + '៦': '6', + '៧': '7', + '៨': '8', + '៩': '9' + } + + // Return the date with western digits + return dateText.replace( + /[٠-٩۰-۹০-৯०-९๐-๙០-៩]/g, + (d) => numeralMappings[d] || d + ) + } + + const removeRtlMarkers = (dateText: string) => { + return dateText.replace(/‏/g, '') + } + + const hasRtlMarkers = (inputValue: string) => { + return inputValue.includes('‏') + } + + const transformDate = ({ + date, + locale, + shouldRemoveRTL = true + }: { + date: Date + locale: string + shouldRemoveRTL?: boolean + }) => { + const formatted = formatDate(date, locale) + const normalized = normalizeWesternDigits(formatted) + const rtlFree = removeRtlMarkers(normalized) + + return shouldRemoveRTL ? rtlFree : normalized + } + + LOCALES.forEach(({ locale, textDirection }) => { + it(`should call onChange with the correct formatted value and ISO date string for locale: ${locale}`, async () => { + const onChange = vi.fn() + // Setting the initial date ensures that the calendar opening on the desired position + const dateForSetInitial = new Date(Date.UTC(2022, 2, 26)) + const dateForExpectSelect = new Date(Date.UTC(2022, 2, 17)) // Thu, 17 Mar 2022 00:00:00 GMT + const expectedDateIsoString = dateForExpectSelect.toISOString() // '2022-03-17T00:00:00.000Z' + const expectedOnChangeValue = transformDate({ + date: dateForExpectSelect, + locale, + shouldRemoveRTL: false + }) + const expectedFormattedValue = transformDate({ + date: dateForExpectSelect, + locale + }) + const initialDate = transformDate({ date: dateForSetInitial, locale }) + const dayForSelect = getDayInOriginalLanguage( + dateForExpectSelect, + locale + ) // 17 (in local language) + + await render( + + ) + + await openCalendar() + + expect(document.querySelector('table')).toBeVisible() + + const day = dayButton(dayForSelect) + + expect(day).toBeEnabled() + + await userEvent.click(day) + + await vi.waitFor(() => { + const inputValue = dateInputElement().value + const inputValueRTLFree = removeRtlMarkers(inputValue) + + // the text direction has to match the locale + expect((textDirection === 'rtl') === hasRtlMarkers(inputValue)).toBe( + true + ) + expect(inputValueRTLFree).toEqual(expectedFormattedValue) + }) + + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + expectedOnChangeValue, + expectedDateIsoString + ) + }) + }) + }) + + it('should change separators according to locale', async () => { + await render() + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, '2022-03 26') + dateInput.blur() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('2022. 03. 26.') + }) + + await userEvent.fill(dateInput, '2022,03/26') + dateInput.blur() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('2022. 03. 26.') + }) + }) + + it('should change leading zero according to locale', async () => { + // every locale gets its own render, so the inputs are looked up in the + // container of the render they belong to + const spanish = await render( + + ) + const spanishInput = spanish.container.querySelector('input')! + + await userEvent.fill(spanishInput, '06.03.2022') + spanishInput.blur() + + await vi.waitFor(() => { + expect(spanishInput).toHaveValue('6/3/2022') + }) + + const polish = await render() + const polishInput = polish.container.querySelector('input')! + + await userEvent.fill(polishInput, '06/3/2022') + polishInput.blur() + + await vi.waitFor(() => { + expect(polishInput).toHaveValue('6.03.2022') + }) + + const afrikaans = await render( + + ) + const afrikaansInput = afrikaans.container.querySelector('input')! + + await userEvent.fill(afrikaansInput, '2022,3,6') + afrikaansInput.blur() + + await vi.waitFor(() => { + expect(afrikaansInput).toHaveValue('2022-03-06') + }) + }) + + it('should dateFormat prop respect the provided local', async () => { + const Example = () => { + const [value, setValue] = useState('') + + return ( + setValue(value)} + /> + ) + } + + // set system date to 2022 march + vi.useFakeTimers({ shouldAdvanceTime: true }) + vi.setSystemTime(new Date(2022, 2, 26)) + + await render() + + expect(dateInputElement()).toHaveValue('') + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue('2022. 03. 17.') + }) + }) + + TIMEZONES_DST.forEach(({ timezone, expectedDateIsoString }) => { + it(`should apply correct timezone and daylight saving adjustments in DST period for: ${timezone}`, async () => { + const onChange = vi.fn() + const initialDate = new Date(Date.UTC(2020, 3, 26)).toLocaleDateString( + 'en-GB' + ) + const expectedFormattedValue = '17/04/2020' + + await render( + + ) + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue(expectedFormattedValue) + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + expectedFormattedValue, + expectedDateIsoString + ) + }) + }) + }) + + TIMEZONES_NON_DST.forEach(({ timezone, expectedDateIsoString }) => { + it(`should apply correct timezone and daylight saving adjustments in non-DST period for: ${timezone}`, async () => { + const onChange = vi.fn() + const initialDate = new Date(Date.UTC(2020, 1, 26)).toLocaleDateString( + 'en-GB' + ) + const expectedFormattedValue = '17/02/2020' + + await render( + + ) + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue(expectedFormattedValue) + expect(onChange).toHaveBeenCalledWith( + expect.anything(), + expectedFormattedValue, + expectedDateIsoString + ) + }) + }) + }) + + it('should set custom value through formatter callback', async () => { + const customValue = 'customValue' + const date = new Date(2020, 10, 10) + + const Example = () => { + const [value, setValue] = useState('') + + return ( + date, + formatter: () => customValue + }} + onChange={(_e, value) => setValue(value)} + /> + ) + } + await render() + + expect(dateInputElement()).toHaveValue('') + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue(customValue) + }) + }) + + it('should render year picker based on the withYearPicker prop', async () => { + // set system date to 2023 march + vi.useFakeTimers({ shouldAdvanceTime: true }) + vi.setSystemTime(new Date(2023, 2, 26)) + + await render( + + ) + + await openCalendar() + + const yearPicker = document.querySelector( + 'input[id^="Select_"]' + )! + + expect(yearPicker).toHaveValue('2023') + expect( + document.querySelector('[id^="Selectable_"][id$="-description"]')! + .textContent + ).toBe('Year picker') + + await userEvent.click(yearPicker) + + await vi.waitFor(() => { + expect(document.querySelector('ul[id^="Selectable_"]')).toBeVisible() + }) + + const options = document.querySelectorAll('[class$="-optionItem"]') + + expect(options).toHaveLength(3) + expect(options[0]).toHaveTextContent('2024') + expect(options[1]).toHaveTextContent('2023') + expect(options[2]).toHaveTextContent('2022') + }) + + it('should set correct value using calendar year picker', async () => { + const Example = () => { + const [value, setValue] = useState('') + + return ( + setValue(value)} + withYearPicker={{ + screenReaderLabel: 'Year picker', + startYear: 2022, + endYear: 2024 + }} + /> + ) + } + + // set system date to 2023 march + vi.useFakeTimers({ shouldAdvanceTime: true }) + vi.setSystemTime(new Date(2023, 2, 26)) + + await render() + + expect(dateInputElement()).toHaveValue('') + + await openCalendar() + + const yearPicker = document.querySelector( + 'input[id^="Select_"]' + )! + + expect(yearPicker).toHaveValue('2023') + + await userEvent.click(yearPicker) + + await vi.waitFor(() => { + expect(document.querySelectorAll('[class$="-optionItem"]')).toHaveLength( + 3 + ) + }) + + await userEvent.click( + document.querySelectorAll('[class$="-optionItem"]')[2] + ) + + await vi.waitFor(() => { + expect(yearPicker).toHaveValue('2022') + }) + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateInputElement()).toHaveValue('17/03/2022') + }) + }) + + it('should display correct year in year picker after date is typed into input', async () => { + const Example = () => { + const [value, setValue] = useState('') + + return ( + setValue(value)} + withYearPicker={{ + screenReaderLabel: 'Year picker', + startYear: 2020, + endYear: 2024 + }} + /> + ) + } + + await render() + + const dateInput = dateInputElement() + + expect(dateInput).toHaveValue('') + + await userEvent.fill(dateInput, '26/03/2021') + dateInput.blur() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('26/03/2021') + }) + + await openCalendar() + + expect( + document.querySelector('input[id^="Select_"]') + ).toHaveValue('2021') + }) + + it('should trigger onRequestValidateDate callback on date selection or blur event', async () => { + const dateValidationSpy = vi.fn() + + await render( + + ) + + await openCalendar() + + await userEvent.click(dayButton('17')) + + await vi.waitFor(() => { + expect(dateValidationSpy).toHaveBeenCalledTimes(1) + }) + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, '26/03/2020') + dateInput.blur() + + await vi.waitFor(() => { + expect(dateValidationSpy).toHaveBeenCalledTimes(2) + }) + }) + + it('should pass necessary props to parser and formatter via dateFormat prop', async () => { + const userDate = '26/03/2021' + const parserReturnedDate = new Date(1111, 11, 11) + + const parserSpy = vi.fn(() => parserReturnedDate) + const formatterSpy = vi.fn(() => '11/11/1111') + + const Example = () => { + const [value, setValue] = useState('') + + return ( + setValue(value)} + /> + ) + } + + await render() + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, userDate) + dateInput.blur() + + await vi.waitFor(() => { + expect(parserSpy).toHaveBeenCalledWith(userDate) + expect(formatterSpy).toHaveBeenCalledWith(parserReturnedDate) + }) + }) + + it('should onRequestValidateDate prop pass necessary props to the callback when input value is not a valid date', async () => { + const dateValidationSpy = vi.fn() + const newValue = 'not a date' + const expectedDateIsoString = '' + + await render( + + ) + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, newValue) + dateInput.blur() + + await vi.waitFor(() => { + expect(dateValidationSpy).toHaveBeenCalledWith( + expect.anything(), + newValue, + expectedDateIsoString + ) + }) + }) + + it('should onRequestValidateDate prop pass necessary props to the callback when input value is a valid date', async () => { + const dateValidationSpy = vi.fn() + const newValue = '26/03/2021' + const expectedDateIsoString = new Date(Date.UTC(2021, 2, 26)).toISOString() + + await render( + + ) + + const dateInput = dateInputElement() + + await userEvent.fill(dateInput, newValue) + dateInput.blur() + + await vi.waitFor(() => { + expect(dateValidationSpy).toHaveBeenCalledWith( + expect.anything(), + newValue, + expectedDateIsoString + ) + }) + }) + + const expectedPlaceholders = [ + { locale: 'hu', expectedPlaceHolder: 'YYYY. MM. DD.' }, + { locale: 'fr', expectedPlaceHolder: 'DD/MM/YYYY' }, + { locale: 'en-US', expectedPlaceHolder: 'M/D/YYYY' }, + { locale: 'ar-SA', expectedPlaceHolder: 'D‏/M‏/YYYY' } + ] + + expectedPlaceholders.forEach(({ locale, expectedPlaceHolder }) => { + it(`should set proper placeholder with locale: ${locale}`, async () => { + await render() + + expect(dateInputElement()).toHaveAttribute( + 'placeholder', + expectedPlaceHolder + ) + }) + }) + + it(`should set proper placeholder with dateFormat prop formatter callback`, async () => { + const expectedPlaceHolder = 'YYYY*M*D' + + const Example = () => { + const [value, setValue] = useState('') + + return ( + { + return new Date(Date.UTC(1111, 11, 11)) + }, + formatter: (date) => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + + // set placeholder according to created date structure 'YYYY*M*D' + return `${year}*${month}*${day}` + } + }} + onChange={(_e, value) => setValue(value)} + /> + ) + } + await render() + + expect(dateInputElement()).toHaveAttribute( + 'placeholder', + expectedPlaceHolder + ) + }) }) diff --git a/packages/ui-date-input/src/DateInput2/__tests__/DateInput2.test.tsx b/packages/ui-date-input/src/DateInput2/__tests__/DateInput2.test.tsx index 78fe60e373..0ebe861e68 100644 --- a/packages/ui-date-input/src/DateInput2/__tests__/DateInput2.test.tsx +++ b/packages/ui-date-input/src/DateInput2/__tests__/DateInput2.test.tsx @@ -22,10 +22,18 @@ * SOFTWARE. */ import { useState } from 'react' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { vi, MockInstance } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, + MockInstance +} from 'vitest' import { HeartInstUIIcon } from '@instructure/ui-icons' import { DateInput2 } from '@instructure/ui-date-input/latest' @@ -68,7 +76,7 @@ describe('', () => { }) it('should render an input', async () => { - const { container } = render() + const { container } = await render() const dateInput = container.querySelector('input') expect(dateInput).toBeInTheDocument() @@ -76,7 +84,7 @@ describe('', () => { }) it('should render an input label', async () => { - const { container } = render() + const { container } = await render() const label = container.querySelector('label') @@ -86,7 +94,7 @@ describe('', () => { it('should render an input placeholder', async () => { const placeholder = 'Placeholder' - render( + await render( ', () => { value="" /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() expect(dateInput).toHaveAttribute('placeholder', placeholder) }) it('should render a calendar icon with screen reader label', async () => { const iconLabel = 'Calendar icon Label' - const { container } = render( + const { container } = await render( ', () => { /> ) const calendarIcon = container.querySelector('svg[name="Calendar"]') - const calendarLabel = screen.getByText(iconLabel) + const calendarLabel = page.getByText(iconLabel).element() expect(calendarIcon).toBeInTheDocument() expect(calendarLabel).toBeInTheDocument() @@ -126,7 +134,7 @@ describe('', () => { it('refs should return the underlying component', async () => { const inputRef = vi.fn() const ref: React.Ref = { current: null } - const { container } = render( + const { container } = await render( ', () => { it('should render a custom calendar icon with screen reader label', async () => { const iconLabel = 'Calendar icon Label' - const { container } = render( + const { container } = await render( ', () => { /> ) const calendarIcon = container.querySelector('svg[name="Heart"]') - const calendarLabel = screen.getByText(iconLabel) + const calendarLabel = page.getByText(iconLabel).element() expect(calendarIcon).toBeInTheDocument() expect(calendarLabel).toBeInTheDocument() }) it('should not show calendar table by default', async () => { - render() - const calendarTable = screen.queryByRole('table') + await render() + const calendarTable = page.getByRole('table').query() expect(calendarTable).not.toBeInTheDocument() }) it('should show calendar table when calendar button is clicked', async () => { - render() - const calendarButton = screen.getByRole('button') + await render() + const calendarButton = page.getByRole('button').element() expect(calendarButton).toBeInTheDocument() await userEvent.click(calendarButton) - await waitFor(() => { - const calendarTable = screen.queryByRole('table') + await vi.waitFor(() => { + const calendarTable = page.getByRole('table').query() expect(calendarTable).toBeInTheDocument() }) }) @@ -191,7 +199,7 @@ describe('', () => { const nextMonthLabel = 'Next month' const prevMonthLabel = 'Previous month' - render( + await render( ', () => { value="" /> ) - const calendarButton = screen.getByRole('button') + const calendarButton = page.getByRole('button').element() await userEvent.click(calendarButton) - await waitFor(() => { - const prevMonthButton = screen.getByRole('button', { - name: prevMonthLabel - }) - const nextMonthButton = screen.getByRole('button', { - name: nextMonthLabel - }) + await vi.waitFor(() => { + const prevMonthButton = page + .getByRole('button', { + name: prevMonthLabel + }) + .element() + const nextMonthButton = page + .getByRole('button', { + name: nextMonthLabel + }) + .element() expect(prevMonthButton).toBeInTheDocument() expect(nextMonthButton).toBeInTheDocument() - const prevButtonLabel = screen.getByText(prevMonthLabel) - const nextButtonLabel = screen.getByText(nextMonthLabel) + const prevButtonLabel = page.getByText(prevMonthLabel).element() + const nextButtonLabel = page.getByText(nextMonthLabel).element() expect(prevButtonLabel).toBeInTheDocument() expect(nextButtonLabel).toBeInTheDocument() @@ -237,7 +249,7 @@ describe('', () => { it('should programmatically set and render the initial value', async () => { const value = '26/03/2024' - render( + await render( ', () => { value={value} /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() expect(dateInput).toHaveValue(value) expect(dateInput).toBeInTheDocument() @@ -258,7 +270,7 @@ describe('', () => { it('should set interaction type to disabled', async () => { const interactionDisabled = 'disabled' - const { container } = render( + const { container } = await render( ', () => { it('should set interaction type to readonly', async () => { const interactionReadOnly = 'readonly' - const { container } = render( + const { container } = await render( ', () => { /> ) const dateInput = container.querySelector('input') - const calendarButton = screen.getByRole('button') + const calendarButton = page.getByRole('button').element() expect(dateInput).toHaveAttribute(interactionReadOnly) expect(calendarButton).toBeInTheDocument() - await userEvent.click(calendarButton) + // `force` because Playwright refuses to click the disabled trigger + await userEvent.click(calendarButton, { force: true }) - await waitFor(() => { - const calendarTable = screen.queryByRole('table') + await vi.waitFor(() => { + const calendarTable = page.getByRole('table').query() expect(calendarTable).not.toBeInTheDocument() }) }) it('should set required', async () => { - const { container } = render( + const { container } = await render( ', () => { it('should call onBlur', async () => { const onBlur = vi.fn() - render( + await render( ', () => { onBlur={onBlur} /> ) - const dateInput = screen.getByLabelText('Choose a date') + const dateInput = page.getByLabelText('Choose a date').element() - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(onBlur).toHaveBeenCalled() }) }) @@ -367,19 +380,19 @@ describe('', () => { ) } - render() + await render() - expect(screen.queryByText(errorMsg)).not.toBeInTheDocument() + expect(page.getByText(errorMsg).query()).not.toBeInTheDocument() - const dateInput = screen.getByLabelText(LABEL_TEXT) + const dateInput = page.getByLabelText(LABEL_TEXT).element() await userEvent.click(dateInput) await userEvent.type(dateInput, 'Not a date') dateInput.blur() - await waitFor(() => { - expect(screen.getByText(errorMsg)).toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText(errorMsg).element()).toBeInTheDocument() }) }) @@ -392,7 +405,7 @@ describe('', () => { { type: 'screenreader-only', text: 'Screenreader' } ] - render( + await render( ', () => { /> ) - expect(screen.getByText('TypeLess')).toBeVisible() - expect(screen.getByText('Error')).toBeVisible() - expect(screen.getByText('Success')).toBeVisible() - expect(screen.getByText('Hint')).toBeVisible() + expect(page.getByText('TypeLess').element()).toBeVisible() + expect(page.getByText('Error').element()).toBeVisible() + expect(page.getByText('Success').element()).toBeVisible() + expect(page.getByText('Hint').element()).toBeVisible() - const screenreaderMessage = screen.getByText('Screenreader') + const screenreaderMessage = page.getByText('Screenreader').element() expect(screenreaderMessage).toBeInTheDocument() expect(screenreaderMessage).toHaveClass(/screenReaderContent/) }) @@ -418,7 +431,7 @@ describe('', () => { it('should render date picker dialog with proper role and ARIA label', async () => { const datePickerLabel = 'Date picker' - render( + await render( ', () => { /> ) - const calendarButton = screen.getByRole('button', { name: 'Calendar' }) + const calendarButton = page + .getByRole('button', { name: 'Calendar' }) + .element() await userEvent.click(calendarButton) - await waitFor(() => { - const dialog = screen.getByRole('dialog', { name: datePickerLabel }) + await vi.waitFor(() => { + const dialog = page + .getByRole('dialog', { name: datePickerLabel }) + .element() expect(dialog).toBeInTheDocument() expect(dialog).toHaveAttribute('aria-label', datePickerLabel) }) diff --git a/packages/ui-date-time-input/src/DateTimeInput/__tests__/DateTimeInput.test.tsx b/packages/ui-date-time-input/src/DateTimeInput/__tests__/DateTimeInput.test.tsx index 8b6f4c6769..cbc947195b 100644 --- a/packages/ui-date-time-input/src/DateTimeInput/__tests__/DateTimeInput.test.tsx +++ b/packages/ui-date-time-input/src/DateTimeInput/__tests__/DateTimeInput.test.tsx @@ -22,12 +22,29 @@ * SOFTWARE. */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { DateTimeInput } from '@instructure/ui-date-time-input/latest' import { ApplyLocale, DateTime } from '@instructure/ui-i18n' -import '@testing-library/jest-dom' + +// the id prefixes keep these unambiguous, the same way the Cypress specs +// looked the inputs up +const dateInputElement = () => + document.querySelector('input[id^="TextInput_"]')! + +const timeInputElement = () => + document.querySelector('input[id^="Select_"]')! + +// the day number is rendered twice (visible + screen reader label), so match +// on the button's text the same way the Cypress `cy.contains` did +const dayButton = (day: string) => + Array.from( + document.querySelectorAll( + 'button[class*="-calendarDay"]' + ) + ).find((button) => button.textContent?.includes(day))! describe('', () => { let consoleWarningMock: ReturnType @@ -48,12 +65,12 @@ describe('', () => { consoleErrorMock.mockRestore() }) - it('should use the default value', () => { + it('should use the default value', async () => { const locale = 'en-US' const timezone = 'US/Eastern' const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - render( + await render( ', () => { defaultValue={dateTime.toISOString()} /> ) - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() expect(dateInput).toHaveValue('5/1/2017') expect(timeInput).toHaveValue('1:30 PM') }) - it('should use the value', () => { + it('should use the value', async () => { const onChange = vi.fn() const locale = 'en-US' const timezone = 'US/Eastern' const dateTime = DateTime.parse('2017-05-01T23:30Z', locale, timezone) - render( + await render( ', () => { onChange={onChange} /> ) - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() expect(dateInput).toHaveValue('5/1/2017') expect(timeInput).toHaveValue('7:30 PM') }) - it('should prefer value to defaultValue', () => { + it('should prefer value to defaultValue', async () => { const locale = 'en-US' const timezone = 'US/Eastern' const value = DateTime.parse('2017-05-01T17:30Z', locale, timezone) const defaultValue = DateTime.parse('2016-04-01T17:00Z', locale, timezone) const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() expect(dateInput).toHaveValue('5/1/2017') expect(timeInput).toHaveValue('1:30 PM') }) - it('should set time to local midnight when only date is set', () => { + it('should set time to local midnight when only date is set', async () => { const locale = 'en-US' const timezone = 'US/Eastern' const dateObj = DateTime.parse('2017-04-01T18:30Z', locale, timezone) const date = dateObj.toISOString().split('T')[0] - render( + await render( ', () => { defaultValue={date} /> ) - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() expect(dateInput).toHaveValue('4/1/2017') expect(timeInput).toHaveValue('12:00 AM') @@ -178,7 +195,7 @@ describe('', () => { it('should call invalidDateTimeMessage if time is set w/o a date and is required', async () => { const invalidDateTimeMessage = vi.fn((_rawd) => 'whoops') - const { container } = render( + const { container } = await render( ', () => { invalidDateTimeMessage={invalidDateTimeMessage} /> ) - const timeInput = screen.getByLabelText('time-input *') + const timeInput = page.getByLabelText('time-input *').element() await userEvent.type(timeInput, '1:00 PM') - fireEvent.blur(timeInput) + fireEvent.focusOut(timeInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('whoops') expect(invalidDateTimeMessage).toHaveBeenCalled() }) @@ -210,7 +227,7 @@ describe('', () => { it('should not call invalidDateTimeMessage if time is set w/o a date', async () => { const invalidDateTimeMessage = vi.fn((_rawd) => 'whoops') - render( + await render( ', () => { invalidDateTimeMessage={invalidDateTimeMessage} /> ) - const timeInput = screen.getByLabelText('time-input') + const timeInput = page.getByLabelText('time-input').element() await userEvent.type(timeInput, '1:00 PM{enter}') - await waitFor(() => { - const errorMessages = screen.queryByText('whoops') + await vi.waitFor(() => { + const errorMessages = page.getByText('whoops').query() expect(errorMessages).not.toBeInTheDocument() expect(invalidDateTimeMessage).not.toHaveBeenCalled() @@ -246,7 +263,7 @@ describe('', () => { const timezone = 'US/Eastern' const defaultValue = DateTime.parse('2017-05-01T17:00', locale, timezone) - render( + await render( ', () => { defaultValue={defaultValue.toISOString()} /> ) - const timeInput = screen.getByLabelText('time-input') + const timeInput = page.getByLabelText('time-input').element() fireEvent.change(timeInput, { target: { value: '3:00 AM' } }) fireEvent.keyDown(timeInput, { key: 'Enter', code: 'Enter' }) - fireEvent.blur(timeInput) + fireEvent.focusOut(timeInput) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][0].target.value).toMatch('3:00 AM') }) }) - it('should show correct message when TimeInput value changes', () => { - const { container } = render( + it('should show correct message when TimeInput value changes', async () => { + const { container } = await render( ', () => { defaultValue="2018-01-18T13:00" /> ) - const timeInput = screen.getByLabelText('time-input') + const timeInput = page.getByLabelText('time-input').element() fireEvent.change(timeInput, { target: { value: '5:00 PM' } }) fireEvent.keyDown(timeInput, { key: 'Enter', code: 'Enter' }) - fireEvent.blur(timeInput) + fireEvent.focusOut(timeInput) - expect(container).toHaveTextContent('Thursday, January 18, 2018 5:00 PM') + await vi.waitFor(() => { + expect(container).toHaveTextContent('Thursday, January 18, 2018 5:00 PM') + }) }) - it('should show the formatted date-time message', () => { + it('should show the formatted date-time message', async () => { const locale = 'en-US' const timezone = 'US/Eastern' const defaultValue = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - const { container } = render( + const { container } = await render( ', () => { expect(container).toHaveTextContent('Monday, May 1, 2017 1:30 PM') }) - it('should show the formatted date-time message in the proper locale', () => { + it('should show the formatted date-time message in the proper locale', async () => { const timezone = 'US/Eastern' const locale = 'fr' const defaultValue = DateTime.parse('2017-05-01T17:30Z', locale, timezone) - const { container } = render( + const { container } = await render( ', () => { expect(container).toHaveTextContent('1 mai 2017 13:30') }) - it('should provide the html elements for date and time input', () => { + it('should provide the html elements for date and time input', async () => { let dref let tref - render( + await render( ', () => { }} /> ) - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() expect(dateInput).toEqual(dref) expect(timeInput).toEqual(tref) }) - it('should update message when value prop changes', () => { + it('should update message when value prop changes', async () => { const onChange = vi.fn() const locale = 'en-US' const timezone = 'US/Eastern' @@ -416,14 +435,14 @@ describe('', () => { onChange } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') - rerender() + await rerender() expect(container).toHaveTextContent('March 29, 2018 12:30 PM') }) - it('should update message when locale changed', () => { + it('should update message when locale changed', async () => { const onChange = vi.fn() const locale = 'en-US' const timezone = 'US/Eastern' @@ -447,14 +466,14 @@ describe('', () => { value: dateTime.toISOString() } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') - rerender() + await rerender() expect(container).toHaveTextContent('1 mai 2017 13:30') }) - it('should update message when timezone changed', () => { + it('should update message when timezone changed', async () => { const onChange = vi.fn() const locale = 'en-US' const timezone = 'US/Eastern' @@ -477,15 +496,15 @@ describe('', () => { value: dateTime.toISOString() } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') - rerender() + await rerender() expect(container).toHaveTextContent('May 1, 2017 7:30 PM') }) it('should show error message if initial value is invalid', async () => { - const { container } = render( + const { container } = await render( ', () => { value="totally not a date" /> ) - const dateInput = screen.getByLabelText('date-input') - fireEvent.blur(dateInput) + const dateInput = page.getByLabelText('date-input').element() + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('whoops') }) }) it('should show error message if initial defaultValue is invalid', async () => { - const { container } = render( + const { container } = await render( ', () => { defaultValue="totally not a date" /> ) - const dateInput = screen.getByLabelText('date-input') - fireEvent.blur(dateInput) + const dateInput = page.getByLabelText('date-input').element() + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('whoops') }) }) @@ -554,14 +573,14 @@ describe('', () => { timezone, value: dateTime.toISOString() } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') - rerender() - const dateInput = screen.getByLabelText('date-input') - fireEvent.blur(dateInput) + await rerender() + const dateInput = page.getByLabelText('date-input').element() + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('whoops') }) }) @@ -586,19 +605,19 @@ describe('', () => { timezone, value: dateTime.toISOString() } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') - rerender( + await rerender( ) - const dateInput = screen.getByLabelText('date-input') - fireEvent.blur(dateInput) + const dateInput = page.getByLabelText('date-input').element() + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('May 1, 2017 1:30 PM') expect(container).toHaveTextContent('message_text') }) @@ -624,28 +643,28 @@ describe('', () => { timezone, value: dateTime.toISOString() } - const { container, rerender } = render( + const { container, rerender } = await render( ) expect(container).not.toHaveTextContent('2017') - rerender( + await rerender( ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(container).not.toHaveTextContent('2017') expect(container).toHaveTextContent('message_text') }) - rerender( + await rerender( ', () => { /> ) - fireEvent.blur(screen.getByLabelText('date-input')) + fireEvent.focusOut(page.getByLabelText('date-input').element()) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('2017') expect(container).toHaveTextContent('message_text') }) }) - it('should read locale and timezone from context', () => { + it('should read locale and timezone from context', async () => { const onChange = vi.fn() const dateTime = DateTime.parse('2017-05-01T17:30Z', 'en-US', 'GMT') - const { container } = render( + const { container } = await render( // Africa/Nairobi is GMT +3 ', () => { } // locale string: French format gives DD/MM/YYYY - const { rerender } = render() - const dateInput = screen.getByLabelText('date-input') + const { rerender } = await render( + + ) + const dateInput = page.getByLabelText('date-input').element() expect(dateInput).toHaveValue('01/05/2017') // custom { parser, formatter } object - rerender( + await rerender( ', () => { /> ) - await waitFor(() => { + await vi.waitFor(() => { expect(dateInput).toHaveValue('2017/05/01') }) }) @@ -756,13 +777,13 @@ describe('', () => { timezone: 'US/Eastern' } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('Monday, May 1, 2017 1:30 PM') - rerender() - fireEvent.blur(screen.getByLabelText('date-input')) + await rerender() + fireEvent.focusOut(page.getByLabelText('date-input').element()) - await waitFor(() => { + await vi.waitFor(() => { expect(container).toHaveTextContent('5/1/2017, 1:30 PM') }) }) @@ -787,18 +808,18 @@ describe('', () => { timezone, value: dateTime.toISOString() } - const { container, rerender } = render() + const { container, rerender } = await render() expect(container).toHaveTextContent('May 1, 2017 1:30 PM') const newDateStr = '2022-03-29T19:00Z' - rerender() - const dateInput = screen.getByLabelText('date-input') - const timeInput = screen.getByLabelText('time-input') + await rerender() + const dateInput = page.getByLabelText('date-input').element() + const timeInput = page.getByLabelText('time-input').element() - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(dateInput).toHaveValue('3/29/2022') expect(timeInput).toHaveValue('3:00 PM') }) @@ -807,7 +828,7 @@ describe('', () => { it("should throw warning if date select and initialTimeForNewDate prop's value is not HH:MM", async () => { const initialTimeForNewDate = 'WRONG_FORMAT' - render( + await render( ', () => { /> ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() fireEvent.change(dateInput, { target: { value: 'May 1, 2017' } }) fireEvent.keyDown(dateInput, { key: 'Enter', code: 'Enter' }) - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(consoleErrorMock.mock.calls[0][0]).toBe( `Warning: [DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.` ) @@ -842,7 +863,7 @@ describe('', () => { it('should throw warning if initialTimeForNewDate prop hour and minute values are not in interval', async () => { const initialTimeForNewDate = '99:99' - render( + await render( ', () => { initialTimeForNewDate={initialTimeForNewDate} /> ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() fireEvent.change(dateInput, { target: { value: 'May 1, 2017' } }) fireEvent.keyDown(dateInput, { key: 'Enter', code: 'Enter' }) - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(consoleErrorMock.mock.calls[0][0]).toEqual( expect.stringContaining( `Warning: [DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.` @@ -881,7 +902,7 @@ describe('', () => { const initialTimeForNewDate = '16:16' const defaultValue = '2018-01-18T13:30' - const { container } = render( + const { container } = await render( ', () => { initialTimeForNewDate={initialTimeForNewDate} /> ) - const timeInput = screen.getByLabelText('time-input') + const timeInput = page.getByLabelText('time-input').element() expect(timeInput).toHaveValue('1:30 PM') expect(container).toHaveTextContent('Thursday, January 18, 2018 1:30 PM') @@ -912,7 +933,7 @@ describe('', () => { // ("LL" by default) as a hint. This test verifies that typing an ISO date string // still works correctly after the switch to DateInput v2. const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, '2017-05-01') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('2017-05-01') expect(dateInput).toHaveValue('5/1/2017') @@ -954,7 +975,7 @@ describe('', () => { const locale = 'en-US' const timezone = 'US/Eastern' - render( + await render( ', () => { /> ) - const calendarButton = screen.getByRole('button', { name: 'Open calendar' }) + const calendarButton = page + .getByRole('button', { name: 'Open calendar' }) + .element() await userEvent.click(calendarButton) - await waitFor(() => { + await vi.waitFor(() => { expect( - screen.getByRole('button', { name: '15 May 2017' }) + page.getByRole('button', { name: '15 May 2017' }).element() ).toBeInTheDocument() }) - await userEvent.click(screen.getByRole('button', { name: '15 May 2017' })) + await userEvent.click(page.getByRole('button', { name: '15 May 2017' })) - await waitFor(() => { - expect(screen.getByLabelText('date-input')).toHaveValue('5/15/2017') - expect(screen.getByLabelText('time-input')).toHaveValue('1:30 PM') + await vi.waitFor(() => { + expect(page.getByLabelText('date-input').element()).toHaveValue( + '5/15/2017' + ) + expect(page.getByLabelText('time-input').element()).toHaveValue('1:30 PM') expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('2017-05-15') }) @@ -999,7 +1024,7 @@ describe('', () => { // before onRequestValidateDate. DateTimeInput must handle this correctly: the // displayed value should be normalized and onChange should fire with the correct ISO. const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, 'May 1 2017') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(dateInput).toHaveValue('5/1/2017') expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('2017-05-01') @@ -1057,14 +1082,14 @@ describe('', () => { it('should accept abbreviated month name (ll format: "Sep 4, 1986")', async () => { const onChange = vi.fn() - renderComponent(onChange) - const dateInput = screen.getByLabelText('date-input') + await renderComponent(onChange) + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, 'Sep 4, 1986') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { - expect(screen.queryByText('whoops')).not.toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText('whoops').query()).not.toBeInTheDocument() expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('1986-09-04') }) @@ -1072,14 +1097,14 @@ describe('', () => { it('should accept numeric date with leading zeros (L format: "09/04/1986")', async () => { const onChange = vi.fn() - renderComponent(onChange) - const dateInput = screen.getByLabelText('date-input') + await renderComponent(onChange) + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, '09/04/1986') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { - expect(screen.queryByText('whoops')).not.toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText('whoops').query()).not.toBeInTheDocument() expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('1986-09-04') }) @@ -1087,14 +1112,14 @@ describe('', () => { it('should accept numeric date without leading zeros (l format: "9/4/1986")', async () => { const onChange = vi.fn() - renderComponent(onChange) - const dateInput = screen.getByLabelText('date-input') + await renderComponent(onChange) + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, '9/4/1986') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { - expect(screen.queryByText('whoops')).not.toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText('whoops').query()).not.toBeInTheDocument() expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('1986-09-04') }) @@ -1102,14 +1127,14 @@ describe('', () => { it('should accept date with time component (LLL format: "September 4, 1986 8:30 PM")', async () => { const onChange = vi.fn() - renderComponent(onChange) - const dateInput = screen.getByLabelText('date-input') + await renderComponent(onChange) + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, 'September 4, 1986 8:30 PM') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { - expect(screen.queryByText('whoops')).not.toBeInTheDocument() + await vi.waitFor(() => { + expect(page.getByText('whoops').query()).not.toBeInTheDocument() expect(onChange).toHaveBeenCalled() expect(onChange.mock.calls[0][1]).toContain('1986-09-05') }) @@ -1117,7 +1142,7 @@ describe('', () => { }) it('should render the year picker in the calendar when withYearPicker is set', async () => { - render( + await render( ', () => { /> ) - const calendarButton = screen.getByRole('button', { name: 'Open calendar' }) + const calendarButton = page + .getByRole('button', { name: 'Open calendar' }) + .element() await userEvent.click(calendarButton) - await waitFor(() => { - const yearPicker = screen.getByRole('combobox', { - description: 'Pick a year' - }) + await vi.waitFor(() => { + // The year picker is looked up through its screen reader description + const description = Array.from( + document.querySelectorAll('[id$="-description"]') + ).find((el) => el.textContent === 'Pick a year')! + const yearPicker = document.querySelector( + `input[aria-describedby~="${description?.id}"]` + ) expect(yearPicker).toBeInTheDocument() expect(yearPicker).toHaveValue('2017') }) @@ -1155,7 +1186,7 @@ describe('', () => { it('should call consumer onChange exactly once when typing a short-format date and blurring', async () => { const onChange = vi.fn() - render( + await render( ', () => { onChange={onChange} /> ) - const dateInput = screen.getByLabelText('date-input') + const dateInput = page.getByLabelText('date-input').element() await userEvent.type(dateInput, '9/1/2017') - fireEvent.blur(dateInput) + fireEvent.focusOut(dateInput) - await waitFor(() => { + await vi.waitFor(() => { expect(dateInput).toHaveValue('9/1/2017') expect(onChange).toHaveBeenCalledTimes(1) expect(onChange.mock.calls[0][1]).toContain('2017-09-01') @@ -1200,8 +1231,8 @@ describe('', () => { invalidDateTimeMessage: 'whoops' } as const - it('uses datePlaceholder when provided', () => { - render( + it('uses datePlaceholder when provided', async () => { + await render( ', () => { datePlaceholder="Pick a date" /> ) - expect(screen.getByLabelText('date-input')).toHaveAttribute( + expect(page.getByLabelText('date-input').element()).toHaveAttribute( 'placeholder', 'Pick a date' ) }) - it('shows built-in format hint when datePlaceholder is omitted', () => { - render( + it('shows built-in format hint when datePlaceholder is omitted', async () => { + await render( ) expect( - screen.getByLabelText('date-input').getAttribute('placeholder') + page.getByLabelText('date-input').element().getAttribute('placeholder') ).toBeTruthy() }) }) + + describe('Component tests', () => { + it('should merge defaultValue and initialTimeForNewDate and handle onChange when the user clears date input and select another one', async () => { + const onChange = vi.fn() + const locale = 'en-US' + const timezone = 'US/Eastern' + const initialTimeForNewDate = '16:16' + const defaultValue = '2018-01-18T13:30' + + const { container } = await render( + + ) + expect(container).toHaveTextContent('date time description') + expect(container).toHaveTextContent('date-input label') + expect(container).toHaveTextContent('time-input label') + expect(container).toHaveTextContent('Thursday, January 18, 2018 1:30 PM') + + const dateInput = dateInputElement() + const timeInput = timeInputElement() + + expect(dateInput).toHaveValue('1/18/2018') + expect(timeInput).toHaveValue('1:30 PM') + + await userEvent.clear(dateInput) + dateInput.blur() + + await vi.waitFor(() => { + expect(timeInput).toHaveValue('') + expect(onChange).toHaveBeenCalled() + }) + + await userEvent.click(page.getByRole('button', { name: 'Choose date' })) + + await vi.waitFor(() => { + expect(dayButton('22')).toBeInTheDocument() + }) + await userEvent.click(dayButton('22')) + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalled() + }) + + const selectedDateId: string = onChange.mock.lastCall![1] + const selectedDateValue = new Date(selectedDateId).toLocaleDateString( + 'en-US', + { + timeZone: 'US/Eastern', + calendar: 'gregory', + numberingSystem: 'latn' + } + ) + + await vi.waitFor(() => { + expect(dateInput).toHaveValue(selectedDateValue) + expect(timeInput).toHaveValue('4:16 PM') + }) + + expect(selectedDateId.split('T')[0]).toContain('-22') + }) + + it('should not fire the onDateChange event when DateInput value change is not a date change', async () => { + const onChange = vi.fn() + + await render( + + ) + const dateInput = dateInputElement() + + await userEvent.click(dateInput) + await userEvent.type(dateInput, 'Not a date{Enter}') + dateInput.blur() + + expect(onChange).not.toHaveBeenCalled() + }) + + it('should show an error message when setting a disabled date array', async () => { + const locale = 'en-US' + const timezone = 'US/Eastern' + const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) + const errorMsg = 'Disabled date selected!' + + const { container } = await render( + + ) + const dateInput = dateInputElement() + + expect(container).toHaveTextContent(errorMsg) + + await userEvent.fill(dateInput, '05/18/2017') + dateInput.blur() + + await vi.waitFor(() => { + expect(container).not.toHaveTextContent(errorMsg) + }) + }) + + it('should show an error message when setting a disabled date function', async () => { + const locale = 'en-US' + const timezone = 'US/Eastern' + const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) + const errorMsgText = 'Disabled date selected!' + const errorMsg = (_rawDate?: string) => errorMsgText + const checker = (toCheck: string) => { + return toCheck.includes('2017') + } + + const { container } = await render( + + ) + const dateInput = dateInputElement() + + expect(container).toHaveTextContent(errorMsgText) + + await userEvent.fill(dateInput, 'May 18, 2022') + dateInput.blur() + + await vi.waitFor(() => { + expect(container).not.toHaveTextContent(errorMsgText) + }) + }) + + it('should clear TimeSelect when DateInput is cleared', async () => { + const locale = 'en-US' + const timezone = 'US/Eastern' + const dateTime = DateTime.parse('2017-05-01T17:30Z', locale, timezone) + const invalidDateTimeMessage = 'invalidDateTimeMessage' + const props = { + description: 'date_time', + dateRenderLabel: 'date-input', + screenReaderLabels: { + calendarIcon: 'Open calendar', + prevMonthButton: 'Previous month', + nextMonthButton: 'Next month', + datePickerDialog: 'Date picker', + selectedLabel: 'selected' + }, + timeRenderLabel: 'time-input', + invalidDateTimeMessage: 'whoops', + locale, + timezone + } + + const { container, rerender } = await render( + + ) + + const dateInput = dateInputElement() + const timeInput = timeInputElement() + + expect(dateInput).toHaveValue('5/1/2017') + expect(timeInput).toHaveValue('1:30 PM') + expect(container).toHaveTextContent('May 1, 2017 1:30 PM') + + // 1. clear programmatically + await rerender() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('') + expect(timeInput).toHaveValue('') + expect(container).not.toHaveTextContent('May 1, 2017 1:30 PM') + }) + + // 2. clear via keyboard input + const newDateStr = '2022-03-29T19:00Z' + await rerender() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('3/29/2022') + expect(timeInput).toHaveValue('3:00 PM') + expect(container).toHaveTextContent('March 29, 2022 3:00 PM') + }) + + await userEvent.clear(dateInput) + dateInput.blur() + + await vi.waitFor(() => { + expect(dateInput).toHaveValue('') + expect(timeInput).toHaveValue('') + expect(container).not.toHaveTextContent('March 29, 2022 3:00 PM') + expect(container).not.toHaveTextContent(invalidDateTimeMessage) + }) + }) + + it('should allow the user to enter any time value if allowNonStepInput is true', async () => { + const onChange = vi.fn() + + await render( + + ) + const dateInput = dateInputElement() + const timeInput = timeInputElement() + + await userEvent.fill(timeInput, '7:34 PM') + + await userEvent.fill(dateInput, 'May 1, 2017') + dateInput.blur() + + await vi.waitFor(() => { + expect(onChange).toHaveBeenCalled() + expect(onChange.mock.lastCall![1]).toBe('2017-05-01T23:34:00.000Z') + }) + }) + + it("should change value of TimeSelect to initialTimeForNewDate prop's value", async () => { + const locale = 'en-US' + const timezone = 'US/Eastern' + + await render( + + ) + const dateInput = dateInputElement() + const timeInput = timeInputElement() + + await userEvent.fill(dateInput, 'May 1, 2017') + dateInput.blur() + + await vi.waitFor(() => { + expect(timeInput).toHaveValue('5:05 AM') + }) + }) + }) }) diff --git a/packages/ui-dom-utils/src/__tests__/addEventListener.test.tsx b/packages/ui-dom-utils/src/__tests__/addEventListener.test.tsx index dc720285b3..3a58ccf1a7 100644 --- a/packages/ui-dom-utils/src/__tests__/addEventListener.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/addEventListener.test.tsx @@ -22,16 +22,16 @@ * SOFTWARE. */ -import { fireEvent, render } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect, vi } from 'vitest' import { addEventListener } from '../addEventListener.js' describe('addEventListener', () => { - it('should add an event listener and provide a remove method', () => { + it('should add an event listener and provide a remove method', async () => { const callback = vi.fn() - const { container } = render(
) + const { container } = await render(
) const node = container.firstChild as HTMLDivElement const listener = addEventListener(node, 'click', callback) diff --git a/packages/ui-dom-utils/src/__tests__/addInputModeListener.test.tsx b/packages/ui-dom-utils/src/__tests__/addInputModeListener.test.tsx index d9a936304c..d2da2c21c3 100644 --- a/packages/ui-dom-utils/src/__tests__/addInputModeListener.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/addInputModeListener.test.tsx @@ -22,16 +22,17 @@ * SOFTWARE. */ -import { render, fireEvent, screen } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { addInputModeListener } from '../addInputModeListener.js' describe('addInputModeListener', () => { - it('should handle input mode changes', () => { + it('should handle input mode changes', async () => { const handleInputModeChange = vi.fn() - render( + await render(
@@ -42,7 +43,7 @@ describe('addInputModeListener', () => { onInputModeChange: handleInputModeChange }) - const button = screen.getByRole('button', { name: 'hello' }) + const button = page.getByRole('button', { name: 'hello' }).element() fireEvent.mouseUp(button) expect(inputModeListener.isKeyboardMode()).toBe(false) diff --git a/packages/ui-dom-utils/src/__tests__/addPositionChangeListener.test.tsx b/packages/ui-dom-utils/src/__tests__/addPositionChangeListener.test.tsx index 38d5494fa3..5ede997b8e 100644 --- a/packages/ui-dom-utils/src/__tests__/addPositionChangeListener.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/addPositionChangeListener.test.tsx @@ -22,9 +22,8 @@ * SOFTWARE. */ -import { render, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect, vi } from 'vitest' import { addPositionChangeListener } from '../addPositionChangeListener.js' const mockRect = { @@ -44,7 +43,7 @@ describe('addPositionChangeListener', () => { const callback = vi.fn() Element.prototype.getBoundingClientRect = vi.fn(() => mockRect as DOMRect) - const { container } = render(
) + const { container } = await render(
) const node = container.firstChild as HTMLDivElement const listener = addPositionChangeListener(node, callback) @@ -54,7 +53,7 @@ describe('addPositionChangeListener', () => { return { ...mockRect, top: 200 } as DOMRect }) - await waitFor(() => { + await vi.waitFor(() => { expect(callback).toHaveBeenCalledTimes(1) expect(typeof listener.remove).toBe('function') }) diff --git a/packages/ui-dom-utils/src/__tests__/findTabbable.test.tsx b/packages/ui-dom-utils/src/__tests__/findTabbable.test.tsx index e069cf049b..be3626ba67 100644 --- a/packages/ui-dom-utils/src/__tests__/findTabbable.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/findTabbable.test.tsx @@ -22,14 +22,14 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' import { findTabbable } from '../findTabbable.js' describe('findTabbable', () => { describe('tabbable content', () => { - it('should find tabbable descendants', () => { - const { container } = render( + it('should find tabbable descendants', async () => { + const { container } = await render(
Tabbable
Not Tabbable
@@ -48,9 +48,9 @@ describe('findTabbable', () => { }) describe('tabbable root', () => { - it('should search the root node when shouldSearchRootNode is set', () => { + it('should search the root node when shouldSearchRootNode is set', async () => { const shouldSearchRootNode = true - const { container } = render( + const { container } = await render( diff --git a/packages/ui-dom-utils/src/__tests__/getCSSStyleDeclaration.test.tsx b/packages/ui-dom-utils/src/__tests__/getCSSStyleDeclaration.test.tsx index 9f1f7241ed..7e561bb4e7 100644 --- a/packages/ui-dom-utils/src/__tests__/getCSSStyleDeclaration.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/getCSSStyleDeclaration.test.tsx @@ -22,8 +22,7 @@ * SOFTWARE. */ -import '@testing-library/jest-dom' -import { vi } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { getCSSStyleDeclaration } from '../getCSSStyleDeclaration.js' describe('getCSSStyleDeclaration', () => { diff --git a/packages/ui-dom-utils/src/__tests__/getClassList.test.tsx b/packages/ui-dom-utils/src/__tests__/getClassList.test.tsx index 4327a8e9ce..ee7dd092f8 100644 --- a/packages/ui-dom-utils/src/__tests__/getClassList.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/getClassList.test.tsx @@ -22,13 +22,15 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' import { getClassList } from '../getClassList.js' describe('getClassList', () => { - it('should provide classlist methods', () => { - const { container } = render(Test) + it('should provide classlist methods', async () => { + const { container } = await render( + Test + ) const node = container.firstChild as HTMLElement const classes = getClassList(node) diff --git a/packages/ui-dom-utils/src/__tests__/getFontSize.test.tsx b/packages/ui-dom-utils/src/__tests__/getFontSize.test.tsx index eaa28dee30..18d4fb43f8 100644 --- a/packages/ui-dom-utils/src/__tests__/getFontSize.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/getFontSize.test.tsx @@ -22,13 +22,15 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' import { getFontSize } from '../getFontSize.js' describe('getFontSize', () => { - it('should return font size as a number', () => { - const { container } = render(Test) + it('should return font size as a number', async () => { + const { container } = await render( + Test + ) const node = container.firstChild expect(getFontSize(node)).toBe(17) diff --git a/packages/ui-dom-utils/src/__tests__/getOffsetParents.test.tsx b/packages/ui-dom-utils/src/__tests__/getOffsetParents.test.tsx index 3dfbd04996..58c5bbf47a 100644 --- a/packages/ui-dom-utils/src/__tests__/getOffsetParents.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/getOffsetParents.test.tsx @@ -22,8 +22,8 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect, beforeEach } from 'vitest' import { getScrollParents } from '../getScrollParents.js' describe('getScrollParents', () => { @@ -71,8 +71,8 @@ describe('getScrollParents', () => {
) - beforeEach(() => { - render(node) + beforeEach(async () => { + await render(node) }) it('should find scroll parent for inline elements', () => { diff --git a/packages/ui-dom-utils/src/__tests__/getScrollParents.test.tsx b/packages/ui-dom-utils/src/__tests__/getScrollParents.test.tsx index f57f2a3604..9078785069 100644 --- a/packages/ui-dom-utils/src/__tests__/getScrollParents.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/getScrollParents.test.tsx @@ -22,8 +22,8 @@ * SOFTWARE. */ -import { render } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' import { getOffsetParents } from '../getOffsetParents.js' describe('getOffsetParents', () => { @@ -57,26 +57,26 @@ describe('getOffsetParents', () => {
) - it('should find offset parent for inline elements', () => { - const { getByTestId } = render(node) - const child = getByTestId('child-1') - const parent = getByTestId('child_1_wrapper_parent') + it('should find offset parent for inline elements', async () => { + const { getByTestId } = await render(node) + const child = getByTestId('child-1').element() + const parent = getByTestId('child_1_wrapper_parent').element() const offsetParents = getOffsetParents(child) expect(offsetParents[0]).toBe(parent) }) - it('should ignore static parents when absolute', () => { - const { getByTestId } = render(node) - const child = getByTestId('child-2') + it('should ignore static parents when absolute', async () => { + const { getByTestId } = await render(node) + const child = getByTestId('child-2').element() const offsetParents = getOffsetParents(child) expect(offsetParents.length).toBe(3) }) - it('should handle fixed', () => { - const { getByTestId } = render(node) - const child = getByTestId('child-3') + it('should handle fixed', async () => { + const { getByTestId } = await render(node) + const child = getByTestId('child-3').element() const offsetParents = getOffsetParents(child) expect(offsetParents.length).toBe(1) diff --git a/packages/ui-dom-utils/src/__tests__/isCustomElement.test.tsx b/packages/ui-dom-utils/src/__tests__/isCustomElement.test.tsx index f705ad90c7..8535bf47e7 100644 --- a/packages/ui-dom-utils/src/__tests__/isCustomElement.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/isCustomElement.test.tsx @@ -22,7 +22,7 @@ * SOFTWARE. */ -import '@testing-library/jest-dom' +import { describe, it, expect, beforeAll } from 'vitest' import { isDefinedCustomElement } from '../isDefinedCustomElement.js' class TestElement extends HTMLElement {} diff --git a/packages/ui-dom-utils/src/__tests__/isVisible.test.tsx b/packages/ui-dom-utils/src/__tests__/isVisible.test.tsx index e145d891b7..7c69760b12 100644 --- a/packages/ui-dom-utils/src/__tests__/isVisible.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/isVisible.test.tsx @@ -22,64 +22,65 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect } from 'vitest' import { isVisible } from '../isVisible.js' describe('isVisible', () => { - it('should recognize visible elements', () => { - render(
Hello world!
) - const element = screen.getByTestId('test') + it('should recognize visible elements', async () => { + await render(
Hello world!
) + const element = page.getByTestId('test').element() expect(isVisible(element)).toBe(true) }) - it('should recognize elements with display: none', () => { - render( + it('should recognize elements with display: none', async () => { + await render(
Hello world!
) - const element = screen.getByTestId('test').firstChild + const element = page.getByTestId('test').element().firstChild expect(isVisible(element)).toBe(false) }) - it('should recognize elements hidden with clip', () => { + it('should recognize elements hidden with clip', async () => { const style: React.CSSProperties = { position: 'absolute', overflow: 'hidden', clip: 'rect(0,0,0,0)' } - render( + await render(
Hello world!
) - const element = screen.getByTestId('test').firstChild + const element = page.getByTestId('test').element().firstChild expect(isVisible(element)).toBe(false) }) - it('should recognize clipped elements that are not hidden', () => { + it('should recognize clipped elements that are not hidden', async () => { const style: React.CSSProperties = { position: 'absolute', overflow: 'hidden', clip: 'rect(0,0,10px,0)' } - render( + await render(
Hello world!
) - const element = screen.getByTestId('test').firstChild + const element = page.getByTestId('test').element().firstChild expect(isVisible(element)).toBe(true) }) - it('should recursively check parent visibility', () => { + it('should recursively check parent visibility', async () => { const style: React.CSSProperties = { visibility: 'hidden' } - render( + await render(
@@ -88,15 +89,15 @@ describe('isVisible', () => {
) - const element = screen.getByTestId('test-2') + const element = page.getByTestId('test-2').element() expect(isVisible(element, false)).toBe(true) expect(isVisible(element)).toBe(false) }) - it('should not recursively check text nodes', () => { + it('should not recursively check text nodes', async () => { const style: React.CSSProperties = { visibility: 'hidden' } - render( + await render(
@@ -105,7 +106,7 @@ describe('isVisible', () => {
) - const element = screen.getByTestId('test-2').firstChild + const element = page.getByTestId('test-2').element().firstChild expect(isVisible(element, false)).toBe(true) expect(isVisible(element)).toBe(false) diff --git a/packages/ui-dom-utils/src/__tests__/requestAnimationFrame.test.tsx b/packages/ui-dom-utils/src/__tests__/requestAnimationFrame.test.tsx index 4217172fcb..4f9d92d7e8 100644 --- a/packages/ui-dom-utils/src/__tests__/requestAnimationFrame.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/requestAnimationFrame.test.tsx @@ -22,8 +22,7 @@ * SOFTWARE. */ -import { waitFor } from '@testing-library/react' -import { vi } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { requestAnimationFrame } from '../requestAnimationFrame.js' describe('requestAnimationFrame', () => { @@ -31,7 +30,7 @@ describe('requestAnimationFrame', () => { const callback = vi.fn() const raf = requestAnimationFrame(callback) - await waitFor(() => { + await vi.waitFor(() => { expect(callback).toHaveBeenCalledTimes(1) expect(typeof raf.cancel).toBe('function') }) diff --git a/packages/ui-dom-utils/src/__tests__/transformSelection.test.tsx b/packages/ui-dom-utils/src/__tests__/transformSelection.test.tsx index 3424db7160..2248fb484e 100644 --- a/packages/ui-dom-utils/src/__tests__/transformSelection.test.tsx +++ b/packages/ui-dom-utils/src/__tests__/transformSelection.test.tsx @@ -22,6 +22,7 @@ * SOFTWARE. */ +import { describe, it, expect } from 'vitest' import { transformSelection, transformCursor } from '../transformSelection.js' describe('transformSelection', () => { diff --git a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerContent.test.tsx b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerContent.test.tsx index 8a039040d9..3a84f55ed0 100644 --- a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerContent.test.tsx +++ b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerContent.test.tsx @@ -22,16 +22,18 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' import { DrawerContent } from '@instructure/ui-drawer-layout/latest' describe('', () => { it('should render', async () => { - render(Hello World) - const drawerContent = screen.getByLabelText('DrawerContentTest') + await render( + Hello World + ) + const drawerContent = page.getByLabelText('DrawerContentTest').element() expect(drawerContent).toBeInTheDocument() expect(drawerContent).toHaveTextContent('Hello World') @@ -39,32 +41,33 @@ describe('', () => { it('should call the content ref', async () => { const contentRef = vi.fn() - render( + await render( Hello World ) - const drawerContent = screen.getByLabelText('DrawerContentTest') + const drawerContent = page.getByLabelText('DrawerContentTest').element() expect(contentRef).toHaveBeenCalledWith(drawerContent) }) it('should not transition on mount, just on update', async () => { - const { rerender } = render( + const { rerender } = await render( Hello World ) - const drawerContent = screen.getByLabelText('DrawerContentTest') + const drawerContent = page.getByLabelText('DrawerContentTest').element() + // in a real browser the missing transition shows up as a zero duration const styleOnMount = getComputedStyle(drawerContent) - expect(styleOnMount.transition).toBe('') + expect(styleOnMount.transitionDuration).toBe('0s') - rerender(Hello World) + await rerender(Hello World) - await waitFor(() => { - const drawerContentUpdated = screen.getByLabelText('test') + await vi.waitFor(() => { + const drawerContentUpdated = page.getByLabelText('test').element() const updatedStyle = getComputedStyle(drawerContentUpdated) - expect(updatedStyle.transition).not.toBe('') + expect(updatedStyle.transitionDuration).not.toBe('0s') }) }) }) diff --git a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerLayout.test.tsx b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerLayout.test.tsx index 4fbdbc1f08..078b292c48 100644 --- a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerLayout.test.tsx +++ b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerLayout.test.tsx @@ -22,16 +22,19 @@ * SOFTWARE. */ -import { render, screen, waitFor, fireEvent, act } from '@testing-library/react' -import { vi } from 'vitest' +import { fireEvent } from '@testing-library/dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' +import { px, within } from '@instructure/ui-utils' import DrawerLayoutFixture from '../v2/__fixtures__/DrawerLayout.fixture.js' import { Button } from '@instructure/ui-buttons/latest' import { DrawerLayout } from '@instructure/ui-drawer-layout/latest' import { View } from '@instructure/ui-view/latest' describe('', () => { - it('should render', () => { - const { container } = render() + it('should render', async () => { + const { container } = await render() const layout = container.querySelector('div[class$="-drawerLayout"]') @@ -39,11 +42,11 @@ describe('', () => { }) it('should render a DrawerTray and DrawerContent', async () => { - const { container } = render( + const { container } = await render( ) - const tray = screen.getByText('Hello from tray') + const tray = page.getByText('Hello from tray').element() const contentWrapper = container.querySelector( 'div[class$="drawerLayout__content"]' ) @@ -55,7 +58,7 @@ describe('', () => { describe('DrawerTray a11y role', () => { it('should have role="dialog" in overlay mode', async () => { - render( + await render( @@ -68,14 +71,14 @@ describe('', () => { ) - await waitFor(() => { - const drawerTray = screen.getByLabelText('a tray test') + await vi.waitFor(() => { + const drawerTray = page.getByLabelText('a tray test').element() expect(drawerTray).toHaveAttribute('role', 'dialog') }) }) it('should have role="region" when not in overlay mode', async () => { - render( + await render( @@ -88,8 +91,8 @@ describe('', () => { ) - await waitFor(() => { - const drawerTray = screen.getByLabelText('a tray test') + await vi.waitFor(() => { + const drawerTray = page.getByLabelText('a tray test').element() expect(drawerTray).toHaveAttribute('role', 'region') }) }) @@ -126,16 +129,205 @@ describe('', () => { ) - const { rerender } = render() - expect(screen.getByText(msg)).toBeInTheDocument() - await act(() => new Promise((resolve) => requestAnimationFrame(resolve))) + const { rerender } = await render() + expect(page.getByText(msg).element()).toBeInTheDocument() + await new Promise((resolve) => requestAnimationFrame(resolve)) fireEvent.keyUp(document, { keyCode: 27 }) // ESC key - await waitFor(() => { + await vi.waitFor(() => { expect(onDismiss).toHaveBeenCalled() }) - rerender() - await waitFor(() => { - expect(screen.queryByText(msg)).not.toBeInTheDocument() + await rerender() + await vi.waitFor(() => { + expect(page.getByText(msg).query()).not.toBeInTheDocument() + }) + }) + + describe('Component tests', () => { + const contentWrapper = (container: HTMLElement) => + container.querySelector( + 'div[class$="-drawerLayout__content"]' + )! + + it('with no overlay, layout content should have margin equal to tray width with placement=start', async () => { + const { container } = await render( + + ) + const tray = page.getByText('Hello from tray').element() + + await vi.waitFor(() => { + const trayWidth = px(getComputedStyle(tray).width) + const marginLeft = px( + getComputedStyle(contentWrapper(container)).marginLeft + ) + + expect(within(marginLeft, 250, 2)).toBe(true) + expect(marginLeft).toBe(trayWidth) + }) + }) + + it('with no overlay, layout content should have margin equal to tray width with placement=end', async () => { + const { container } = await render( + + ) + const tray = page.getByText('Hello from tray').element() + + await vi.waitFor(() => { + const trayWidth = px(getComputedStyle(tray).width) + const marginRight = px( + getComputedStyle(contentWrapper(container)).marginRight + ) + + expect(within(marginRight, 250, 2)).toBe(true) + expect(marginRight).toBe(trayWidth) + }) + }) + + it('with overlay, layout content should have a margin of zero with placement=start', async () => { + const { container } = await render( + + ) + + await vi.waitFor(() => { + const marginLeft = px( + getComputedStyle(contentWrapper(container)).marginLeft + ) + + expect(marginLeft).toBe(0) + }) + }) + + it('with overlay, layout content should have a margin of zero with placement=end', async () => { + const { container } = await render( + + ) + + await vi.waitFor(() => { + const marginRight = px( + getComputedStyle(contentWrapper(container)).marginRight + ) + + expect(marginRight).toBe(0) + }) + }) + + it('the tray should overlay the content when the content is less than the minWidth', async () => { + const onOverlayTrayChange = vi.fn() + + const { rerender } = await render( + + ) + + // set prop layoutWidth + await rerender( + + ) + + await vi.waitFor(() => { + expect(onOverlayTrayChange).toHaveBeenCalledWith(true) + }) + }) + + it('the tray should stop overlaying the content when there is enough space for the content', async () => { + const onOverlayTrayChange = vi.fn() + + const { rerender } = await render( + + ) + + // set prop layoutWidth + await rerender( + + ) + + await vi.waitFor(() => { + expect(onOverlayTrayChange).toHaveBeenCalledWith(false) + }) + }) + + it('the tray should be set to overlay when it is opened and there is not enough space', async () => { + const onOverlayTrayChange = vi.fn() + + const { rerender } = await render( + + ) + + // set prop open + await rerender( + + ) + + await vi.waitFor(() => { + expect(onOverlayTrayChange).toHaveBeenCalledWith(true) + }) + }) + + it('the tray should not overlay on open when there is enough space', async () => { + const onOverlayTrayChange = vi.fn() + + const { rerender } = await render( + + ) + + // set prop open + await rerender( + + ) + + await vi.waitFor(() => { + expect(onOverlayTrayChange).toHaveBeenCalledWith(false) + }) }) }) }) diff --git a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerTray.test.tsx b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerTray.test.tsx index 649c2c9300..5270b14777 100644 --- a/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerTray.test.tsx +++ b/packages/ui-drawer-layout/src/DrawerLayout/__tests__/DrawerTray.test.tsx @@ -22,15 +22,19 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' + +import canvas from '@instructure/ui-themes' +import { InstUISettingsProvider } from '@instructure/emotion' import { DrawerTray } from '@instructure/ui-drawer-layout/latest' +import { DrawerLayoutContext } from '../v2/index.js' describe('', () => { it('should render tray content when open', async () => { - render( + await render( ', () => { }} /> ) - const drawerTray = screen.getByLabelText('DrawerTray Example') + const drawerTray = page.getByLabelText('DrawerTray Example').element() expect(drawerTray).toBeInTheDocument() expect(drawerTray).toHaveTextContent('Hello from layout tray') @@ -47,7 +51,7 @@ describe('', () => { it('should call the contentRef', async () => { const contentRef = vi.fn() - render( + await render( ', () => { }} /> ) - const drawerTray = screen.getByLabelText('DrawerTray Example') + const drawerTray = page.getByLabelText('DrawerTray Example').element() expect(contentRef).toHaveBeenCalledWith(drawerTray.parentElement) }) @@ -65,7 +69,7 @@ describe('', () => { it('should call onOpen', async () => { const onOpen = vi.fn() - const { rerender } = render( + const { rerender } = await render( ', () => { expect(onOpen).not.toHaveBeenCalled() // set prop open - rerender( + await rerender( ', () => { /> ) - await waitFor(() => { + await vi.waitFor(() => { expect(onOpen).toHaveBeenCalled() }) }) it('should call onOpen when open initially', async () => { const onOpen = vi.fn() - render( + await render( ', () => { /> ) - await waitFor(() => { + await vi.waitFor(() => { expect(onOpen).toHaveBeenCalled() }) }) @@ -116,7 +120,7 @@ describe('', () => { it('should call onClose', async () => { const onClose = vi.fn() - const { rerender } = render( + const { rerender } = await render( ', () => { expect(onClose).not.toHaveBeenCalled() // set prop open - rerender( + await rerender( ', () => { /> ) - await waitFor(() => { + await vi.waitFor(() => { expect(onClose).toHaveBeenCalled() }) }) + + describe('Component tests', () => { + const trayContent = () => + document.querySelector('div[class$="-drawerTray__content"]') + + it('should render tray content when open', async () => { + await render( + { + return 'Hello from layout tray' + }} + /> + ) + + expect(trayContent()).toHaveTextContent('Hello from layout tray') + }) + + it('should not render tray content when closed', async () => { + await render( + { + return 'Hello from layout tray' + }} + /> + ) + + expect(trayContent()).not.toBeInTheDocument() + }) + + it('should place the tray correctly with placement=start', async () => { + await render( + { + return 'Hello from layout tray' + }} + /> + ) + + const tray = trayContent()!.parentElement! + + expect(getComputedStyle(tray).left).toBe('0px') + }) + + it('should place the tray correctly with placement=end', async () => { + await render( + { + return 'Hello from layout tray' + }} + /> + ) + + const tray = trayContent()!.parentElement! + + expect(getComputedStyle(tray).right).toBe('0px') + }) + + it('should apply theme overrides when open', async () => { + await render( + { + return 'Hello from layout tray' + }} + /> + ) + + const tray = trayContent()!.parentElement! + + expect(getComputedStyle(tray).zIndex).toBe('333') + }) + + it('drops a shadow if the prop is set, and it is overlaying content', async () => { + const onEntered = vi.fn() + await render( + + + { + return 'Hello from layout tray' + }} + /> + + + ) + + const trayWithShadow = document.querySelector( + 'div[class*="-drawerTray--with-shadow"]' + )! + + expect(getComputedStyle(trayWithShadow).boxShadow).not.toBe('none') + }) + }) }) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx index b19a96fb62..95fd4f8738 100644 --- a/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx +++ b/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx @@ -22,10 +22,10 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { fireEvent } from '@testing-library/dom' +import { render, cleanup } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { runAxeCheck } from '@instructure/ui-axe-check' import { CheckInstUIIcon } from '@instructure/ui-icons' @@ -61,7 +61,7 @@ describe('', () => { describe('rootPageId prop', () => { it('should set the initial page and render it', async () => { - render( + await render( Option-01 @@ -72,9 +72,9 @@ describe('', () => { ) - const drilldown = screen.getByRole('menu') - const options = screen.getAllByRole('menuitem') - const rootlessOption = screen.queryByText('Option-01') + const drilldown = page.getByRole('menu').element() + const options = page.getByRole('menuitem').elements() + const rootlessOption = page.getByText('Option-01').query() expect(drilldown).toBeInTheDocument() expect(rootlessOption).not.toBeInTheDocument() @@ -87,15 +87,15 @@ describe('', () => { describe('children prop', () => { it('should not allow non-DrilldownPage children', async () => { - render( + await render(
DIV-child
) - const drilldown = screen.queryByRole('menu') - const options = screen.queryAllByRole('menuitem') - const notAllowedChild = screen.queryByText('DIV-child') + const drilldown = page.getByRole('menu').query() + const options = page.getByRole('menuitem').elements() + const notAllowedChild = page.getByText('DIV-child').query() expect(drilldown).not.toBeInTheDocument() expect(options.length).toBe(0) @@ -105,7 +105,7 @@ describe('', () => { it('should not crash for weird option ids', async () => { const onSelect = vi.fn() const weirdID = 'some"_weird!@#$%^&*()\\|`id' - render( + await render( {data.map((option) => ( @@ -121,10 +121,10 @@ describe('', () => { ) - const option_1 = screen.getByTestId(weirdID + 'opt_1') + const option_1 = page.getByTestId(weirdID + 'opt_1').element() await userEvent.click(option_1) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).toHaveBeenCalled() }) }) @@ -132,7 +132,7 @@ describe('', () => { describe('id prop', () => { it('should put id attr on the drilldown', async () => { - render( + await render( Option @@ -140,7 +140,7 @@ describe('', () => { ) - const drilldown = screen.getByRole('menu') + const drilldown = page.getByRole('menu').element() expect(drilldown).toBeInTheDocument() expect(drilldown).toHaveAttribute('id', 'testId') @@ -149,7 +149,7 @@ describe('', () => { describe('label prop', () => { it('should be added as aria-label', async () => { - render( + await render( Option @@ -157,7 +157,7 @@ describe('', () => { ) - const drilldown = screen.getByRole('menu') + const drilldown = page.getByRole('menu').element() expect(drilldown).toBeInTheDocument() expect(drilldown).toHaveAttribute('aria-label', 'testLabel') @@ -166,7 +166,7 @@ describe('', () => { describe('disabled prop', () => { it('should disable all options', async () => { - render( + await render( Option @@ -175,7 +175,7 @@ describe('', () => { ) - const options = screen.getAllByRole('menuitem') + const options = page.getByRole('menuitem').elements() expect(options.length).toBe(4) // header action + 3 options @@ -185,7 +185,7 @@ describe('', () => { }) it('should not allow selection if the main Drilldown is disabled', async () => { - render( + await render( @@ -194,8 +194,10 @@ describe('', () => { ) - const optionItemContainer = screen.getByLabelText('Disabled Option') - const optionContent = screen.getByText('Disabled Option') + const optionItemContainer = page + .getByLabelText('Disabled Option') + .element() + const optionContent = page.getByText('Disabled Option').element() expect(optionItemContainer).toHaveAttribute('aria-checked', 'false') @@ -205,7 +207,7 @@ describe('', () => { }) it('should always allow back navigation even if the page is disabled', async () => { - render( + await render( @@ -221,19 +223,19 @@ describe('', () => { ) // 1. Navigate to the disabled page - await userEvent.click(screen.getByText('Go to Disabled Page')) - expect(screen.getByText('Disabled Page')).toBeInTheDocument() + await userEvent.click(page.getByText('Go to Disabled Page').element()) + expect(page.getByText('Disabled Page').element()).toBeInTheDocument() - await userEvent.click(screen.getByText('Back')) + await userEvent.click(page.getByText('Back').element()) // 4. Verify we have successfully navigated back - expect(screen.getByText('First Page')).toBeInTheDocument() + expect(page.getByText('First Page').element()).toBeInTheDocument() }) }) describe('as prop', () => { it('should be "ul" by default', async () => { - const { container } = render( + const { container } = await render( Option @@ -246,7 +248,7 @@ describe('', () => { }) it('should render as passed element', async () => { - const { container } = render( + const { container } = await render( Option @@ -261,7 +263,7 @@ describe('', () => { describe('role prop', () => { it('should be "menu" by default', async () => { - const { container } = render( + const { container } = await render( Option @@ -274,7 +276,7 @@ describe('', () => { }) it('should apply passed role', async () => { - const { container } = render( + const { container } = await render( Option @@ -291,14 +293,14 @@ describe('', () => { it('should give back the drilldown element when there is no trigger', async () => { const elementRef = vi.fn() - render( + await render( Option ) - const drilldown = screen.getByRole('menu') + const drilldown = page.getByRole('menu').element() expect(drilldown).toBeInTheDocument() expect(elementRef).toHaveBeenCalledWith(drilldown) @@ -306,7 +308,7 @@ describe('', () => { it('should give back the Popover root when drilldown has trigger and is closed', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { ) - const trigger = screen.getByText('Toggle') + const trigger = page.getByText('Toggle').element() const positionId = trigger.getAttribute('data-position-target') const drilldownRoot = container.querySelector( `span[data-position="${positionId}"]` @@ -329,7 +331,7 @@ describe('', () => { it('should give back the the Popover root when drilldown has trigger and is open', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { ) - const trigger = screen.getByText('Toggle') + const trigger = page.getByText('Toggle').element() const positionId = trigger.getAttribute('data-position-target') const drilldownRoot = container.querySelector( `span[data-position="${positionId}"]` @@ -355,21 +357,21 @@ describe('', () => { describe('drilldownRef prop', () => { it('should give back the drilldown element when there is no trigger', async () => { const drilldownRef = vi.fn() - render( + await render( Option ) - const drilldown = screen.getByRole('menu') + const drilldown = page.getByRole('menu').element() expect(drilldownRef).toHaveBeenCalledWith(drilldown) }) it("shouldn't be called when drilldown has trigger and is closed", async () => { const drilldownRef = vi.fn() - render( + await render( ', () => { it('should give back the drilldown element when drilldown has trigger and is open', async () => { const drilldownRef = vi.fn() - render( + await render( ', () => { ) - const drilldown = screen.getByRole('menu') + const drilldown = page.getByRole('menu').element() expect(drilldownRef).toHaveBeenCalledWith(drilldown) }) @@ -407,7 +409,7 @@ describe('', () => { describe('popoverRef prop', () => { it('should not be called when there is no trigger', async () => { const popoverRef = vi.fn() - render( + await render( Option @@ -420,7 +422,7 @@ describe('', () => { it('should give back the Popover component when drilldown has trigger and is closed', async () => { const popoverRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { ) - const trigger = screen.getByText('Toggle') + const trigger = page.getByText('Toggle').element() const positionId = trigger.getAttribute('data-position-target') const popoverRoot = container.querySelector( `span[data-position="${positionId}"]` @@ -446,7 +448,7 @@ describe('', () => { it('should give back the Popover component when drilldown has trigger and is open', async () => { const popoverRef = vi.fn() - const { container } = render( + const { container } = await render( ', () => { ) - const trigger = screen.getByText('Toggle') + const trigger = page.getByText('Toggle').element() const positionId = trigger.getAttribute('data-position-target') const popoverRoot = container.querySelector( `span[data-position="${positionId}"]` @@ -475,7 +477,7 @@ describe('', () => { describe('onSelect prop', () => { it('should fire when option is selected', async () => { const onSelect = vi.fn() - render( + await render( {data.map((option) => ( @@ -491,11 +493,11 @@ describe('', () => { ) - const option_1 = screen.getByTestId('opt_1') + const option_1 = page.getByTestId('opt_1').element() await userEvent.click(option_1) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).toHaveBeenCalled() const args = onSelect.mock.calls[0][1] @@ -511,13 +513,14 @@ describe('', () => { expect(args.drilldown.props).toHaveProperty('role', 'menu') expect(args.drilldown.hide).toBeInstanceOf(Function) - expect(event.target).toBe(option_1) + // a real click lands on the innermost label element + expect(option_1).toContainElement(event.target) }) }) it('should not fire when drilldown is disabled', async () => { const onSelect = vi.fn() - render( + await render( {data.map((option) => ( @@ -532,11 +535,10 @@ describe('', () => { ) - const option_1 = screen.getByTestId('opt_1') + // the option itself is aria-disabled, so click its label content + await userEvent.click(page.getByText('option 1')) - await userEvent.click(option_1) - - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).not.toHaveBeenCalled() }) }) @@ -544,14 +546,14 @@ describe('', () => { describe('with a trigger', () => { it('should not show content by default', async () => { - render( + await render( click me}> Option 0 ) - const option_0 = screen.queryByText('Option 0') + const option_0 = page.getByText('Option 0').query() expect(option_0).not.toBeInTheDocument() }) @@ -561,7 +563,7 @@ describe('', () => { container.setAttribute('data-testid', 'container') document.body.appendChild(container) - render( + await render( ', () => { ) - const optionsContainer = screen.getByTestId('container') - const trigger = screen.getByRole('button') + const optionsContainer = page.getByTestId('container').element() + const trigger = page.getByRole('button').element() expect(optionsContainer).not.toHaveTextContent('Option 0') await userEvent.click(trigger) - await waitFor(() => { - const updatedOptionsContainer = screen.getByTestId('container') + await vi.waitFor(() => { + const updatedOptionsContainer = page.getByTestId('container').element() expect(updatedOptionsContainer).toHaveTextContent('Option 0') }) }) it('should have an aria-haspopup attribute', async () => { - render( + await render( Options}> Option 0 ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() expect(trigger).toHaveAttribute('aria-haspopup') }) it('should call onToggle when Drilldown is opened', async () => { const onToggle = vi.fn() - render( + await render( Options} @@ -613,11 +615,11 @@ describe('', () => { ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() await userEvent.click(trigger) - await waitFor(() => { + await vi.waitFor(() => { expect(onToggle).toHaveBeenCalled() const args = onToggle.mock.calls[0][1] @@ -635,7 +637,7 @@ describe('', () => { it('should call onToggle when Drilldown is closed', async () => { const onToggle = vi.fn() - render( + await render( Options} @@ -647,11 +649,11 @@ describe('', () => { ) - const trigger = screen.getByRole('button') + const trigger = page.getByRole('button').element() await userEvent.click(trigger) - await waitFor(() => { + await vi.waitFor(() => { expect(onToggle).toHaveBeenCalled() const args = onToggle.mock.calls[0][1] @@ -669,7 +671,7 @@ describe('', () => { describe('placement prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -692,7 +694,7 @@ describe('', () => { describe('defaultShow prop', () => { it('should display Popover on render', async () => { - render( + await render( Toggle} @@ -703,7 +705,7 @@ describe('', () => {
) - const popoverContent = screen.getByText('Option') + const popoverContent = page.getByText('Option').element() expect(popoverContent).toBeVisible() }) @@ -712,7 +714,7 @@ describe('', () => { describe('show prop', () => { it('should display popover', async () => { const onToggle = vi.fn() - render( + await render( Toggle} @@ -724,7 +726,7 @@ describe('', () => { ) - const popoverContent = screen.getByText('Option') + const popoverContent = page.getByText('Option').element() expect(popoverContent).toBeVisible() }) @@ -734,7 +736,7 @@ describe('', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null const onFocus = vi.fn() - render( + await render( Toggle} @@ -759,7 +761,7 @@ describe('', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null const onMouseOver = vi.fn() - render( + await render( Toggle} @@ -783,7 +785,7 @@ describe('', () => { describe('shouldContainFocus prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -807,7 +809,7 @@ describe('', () => { describe('shouldReturnFocus prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -831,7 +833,7 @@ describe('', () => { describe('withArrow prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -855,7 +857,7 @@ describe('', () => { describe('offsetX prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -879,7 +881,7 @@ describe('', () => { describe('offsetY prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -903,7 +905,7 @@ describe('', () => { describe('positionContainerDisplay prop', () => { it('should be passed to Popover', async () => { let popoverRef: Popover | null = null - render( + await render( Toggle} @@ -926,7 +928,7 @@ describe('', () => { describe('for a11y', () => { it('should be accessible', async () => { - const { container } = render( + const { container } = await render( ', () => { }) it('should meet a11y standarts when drilldown is open', async () => { - const { container } = render( + const { container } = await render( Option 0 @@ -1014,4 +1016,611 @@ describe('', () => { expect(axeCheck).toBe(true) }) }) + + describe('Component tests', () => { + const renderOptions = (pageName: string) => + data.map((option) => ( + + {option.label} - {pageName} + + )) + + const menuEl = () => document.querySelector('div[role="menu"]') + const drilldownContainer = () => + document.querySelector('[class$="-drilldown__container"]')! + const headerTitle = () => + document.querySelector('[id^="DrilldownHeader-Title_"]') + const activeId = () => document.activeElement?.id + + afterEach(async () => { + // the Popover's FocusRegion schedules an async focus return on unmount, + // let it drain so it can't disturb the next test's keyboard navigation + cleanup() + await new Promise((resolve) => setTimeout(resolve, 100)) + }) + + it('should disabled prop prevent option actions', async () => { + await render( + + + + Option-0 + + + + Option-1 + + + ) + + await userEvent.click(page.getByText('Option-0')) + + expect(page.getByText('Option-0').element()).toBeVisible() + expect(page.getByText('Option-1').query()).not.toBeInTheDocument() + }) + + it('should disabled trigger, if disabled prop provided', async () => { + await render( + Toggle} + > + + Option + + + ) + const toggleButton = document.querySelector( + '[data-test-id="toggleButton"]' + )! + + expect(toggleButton).toHaveAttribute('aria-disabled', 'true') + expect(toggleButton).toBeDisabled() + + // a real click can't be sent to a disabled button, so dispatch the event + fireEvent.click(toggleButton, { button: 0, detail: 1 }) + + expect(document.querySelector('#page0option')).not.toBeInTheDocument() + }) + + it('should rotate focus in the drilldown by default', async () => { + await render( + + + Option1 + Option2 + + + ) + menuEl()!.focus() + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('option01')) + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('option02')) + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('option01')) + }) + + it('should prevent focus rotation in the drilldown with "false"', async () => { + await render( + + + Option + Option + + + ) + menuEl()!.focus() + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('option01')) + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('option02')) + + await userEvent.keyboard('{ArrowDown}') + expect(activeId()).toBe('option02') + }) + + it('should set the width of the drilldown', async () => { + await render( + + + Option + + + ) + + expect(getComputedStyle(menuEl()!).width).toBe('320px') + }) + + it('should set the width of the drilldown in the popover', async () => { + await render( + Toggle} + defaultShow + > + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).width).toBe('320px') + }) + + it('should be overruled by maxWidth prop', async () => { + await render( + + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).width).toBe('160px') + }) + + it('should be affected by overflowX prop', async () => { + await render( + + + +
+ Option with a very long label so that it has to break +
+
+
+
+ ) + const container = drilldownContainer() + const style = getComputedStyle(container) + + // 318px, not 320px: the container renders a 1px border each side + // (borderWidth="small") with box-sizing border-box, so the computed + // content width is 320 - 2 = 318. + expect(style.width).toBe('318px') + expect(style.overflowX).toBe('auto') + expect(container.scrollWidth).toBeGreaterThan(container.clientWidth) + }) + + it('should set minWidth in popover mode', async () => { + await render( + Trigger} + show + onToggle={vi.fn()} + > + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).width).toBe('336px') + }) + + it('should set the height of the drilldown', async () => { + await render( + + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).height).toBe('320px') + }) + + it('should set the height of the drilldown in the popover', async () => { + await render( + Toggle} + defaultShow + > + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).height).toBe('320px') + }) + + it('should be overruled by maxHeight prop', async () => { + await render( + + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).height).toBe('160px') + }) + + it('should be affected by overflowY prop', async () => { + await render( + + + Option + Option + Option + Option + Option + Option + + + ) + const container = drilldownContainer() + const style = getComputedStyle(container) + + expect(style.height).toBe('160px') + expect(style.overflowY).toBe('auto') + expect(container.scrollHeight).toBeGreaterThan(container.clientHeight) + }) + + it('should minHeight prop set height', async () => { + await render( + + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).height).toBe('336px') + }) + + it('should minHeight prop set height in popover mode', async () => { + await render( + Trigger} + show + onToggle={vi.fn()} + > + + Option + + + ) + + expect(getComputedStyle(drilldownContainer()).height).toBe('336px') + }) + + it('should call onDismiss when Drilldown is closed', async () => { + const onDismiss = vi.fn() + await render( + Options} + onDismiss={onDismiss} + defaultShow + > + + Option 0 + + + ) + menuEl()!.focus() + + await userEvent.keyboard('{Escape}') + + await vi.waitFor(() => { + expect(onDismiss).toHaveBeenCalled() + expect(onDismiss.mock.calls[0][0]).toBeInstanceOf(Event) + expect(onDismiss.mock.calls[0][1]).toBe(false) + }) + }) + + it('should shouldHideOnSelect prop be true by default', async () => { + await render( + Toggle} + defaultShow + > + + Option-01 + + + ) + expect(document.querySelector('#option01')).toBeInTheDocument() + + await userEvent.click(page.getByText('Option-01')) + + await vi.waitFor(() => { + expect(document.querySelector('#option01')).not.toBeInTheDocument() + }) + }) + + it('should not close on subPage nav, even if shouldHideOnSelect is "true"', async () => { + await render( + Toggle} + defaultShow + shouldHideOnSelect={true} + > + + + Option + + + + Sub-Option + + + ) + expect(document.querySelector('#option01')).toBeInTheDocument() + expect(document.querySelector('#option11')).not.toBeInTheDocument() + + await userEvent.click(page.getByText('Option')) + + await vi.waitFor(() => { + expect(document.querySelector('#option01')).not.toBeInTheDocument() + expect(document.querySelector('#option11')).toBeInTheDocument() + }) + }) + + it('should not close on Back nav, even if shouldHideOnSelect is "true"', async () => { + await render( + Toggle} + defaultShow + shouldHideOnSelect={true} + > + + + Option01 + + + + Sub-Option + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + expect(page.getByText('Sub-Option').element()).toBeVisible() + }) + + await userEvent.click(page.getByText('Back')) + + await vi.waitFor(() => { + expect(page.getByText('Sub-Option').query()).not.toBeInTheDocument() + expect(page.getByText('Option01').element()).toBeVisible() + }) + }) + + it('should prevent closing when shouldHideOnSelect is "false"', async () => { + await render( + Toggle} + defaultShow + shouldHideOnSelect={false} + > + + Option01 + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + expect(page.getByText('Option01').element()).toBeVisible() + }) + + it('should be able to navigate between options with up/down arrows', async () => { + await render( + + + {data.map((option) => ( + + {option.label} + + ))} + + + ) + menuEl()!.focus() + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt_0')) + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt_1')) + + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt_2')) + + await userEvent.keyboard('{ArrowUp}') + await vi.waitFor(() => expect(activeId()).toBe('opt_1')) + }) + + it('should be able to navigate forward between pages with right arrow', async () => { + await render( + + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + menuEl()!.focus() + + // the option which navigates to next page should be focused + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt0')) + expect(document.activeElement).toHaveTextContent('To Page 1') + + // go to Page 1 + await userEvent.keyboard('{ArrowRight}') + await vi.waitFor(() => expect(headerTitle()).toHaveTextContent('Page 1')) + + // focus takes a moment to land on the new page's menu + menuEl()!.focus() + + // on the Page 1 the 1st option is the `Back` button + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => + expect(document.activeElement).toHaveTextContent('Back') + ) + + // next arrowDown should skip the header Title and focus on 'To Page 2' option + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt5')) + expect(document.activeElement).toHaveTextContent('To Page 2') + + // go to Page 2 + await userEvent.keyboard('{ArrowRight}') + + // on Page 2 the header title should be 'Page 2' + await vi.waitFor(() => expect(headerTitle()).toHaveTextContent('Page 2')) + }) + + it('should be able to navigate back to previous page with left arrow', async () => { + await render( + + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + menuEl()!.focus() + + // go to Page 1 + await userEvent.keyboard('{ArrowDown}') + await vi.waitFor(() => expect(activeId()).toBe('opt0')) + await userEvent.keyboard('{ArrowRight}') + + // on Page 1 should be visible header title + await vi.waitFor(() => expect(headerTitle()).toHaveTextContent('Page 1')) + + // focus takes a moment to land on the new page's menu + menuEl()!.focus() + + // go to Page 0 + await userEvent.keyboard('{ArrowLeft}') + + // on Page 0 should be visible header title + await vi.waitFor(() => expect(headerTitle()).toHaveTextContent('Page 0')) + }) + + it('should close the drilldown on root page and left arrow is pressed', async () => { + await render( + options} + defaultShow + > + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + menuEl()!.focus() + expect(headerTitle()).toHaveTextContent('Page 0') + + await userEvent.keyboard('{ArrowLeft}') + + await vi.waitFor(() => { + expect(headerTitle()).not.toBeInTheDocument() + expect(menuEl()).not.toBeInTheDocument() + }) + }) + + it('should correctly return focus when "trigger" and "shouldReturnFocus" is set', async () => { + await render( + Options} + shouldReturnFocus + > + + Option-0 + + + ) + const trigger = page + .getByRole('button', { name: 'Options' }) + .element() as HTMLElement + + trigger.focus() + expect(page.getByText('Option-0').query()).not.toBeInTheDocument() + + await userEvent.keyboard(' ') + + await vi.waitFor(() => + expect(page.getByText('Option-0').element()).toBeVisible() + ) + + await userEvent.keyboard('{Escape}') + + await vi.waitFor(() => { + expect(page.getByText('Option-0').query()).not.toBeInTheDocument() + expect(document.activeElement).toBe(trigger) + }) + }) + }) }) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownGroup.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownGroup.test.tsx index 80b5a811cb..c8dafc2148 100644 --- a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownGroup.test.tsx +++ b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownGroup.test.tsx @@ -22,11 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' - -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { Drilldown } from '@instructure/ui-drilldown/latest' @@ -76,23 +74,23 @@ describe('', () => { ) } - render() + await render() - const selectedOption = screen.getByTestId('three') + const selectedOption = page.getByTestId('three').element() expect(selectedOption).toHaveAttribute('aria-checked', 'true') await userEvent.click(selectedOption) - await waitFor(() => { - const updatedSelectedOption = screen.getByTestId('three') + await vi.waitFor(() => { + const updatedSelectedOption = page.getByTestId('three').element() expect(updatedSelectedOption).toHaveAttribute('aria-checked', 'true') }) }) it("shouldn't render non-DrilldownGroup children", async () => { - render( + await render( @@ -101,8 +99,8 @@ describe('', () => { ) - const nonGroupChild = screen.queryByTestId('testDiv') - const childText = screen.queryByText('Div') + const nonGroupChild = page.getByTestId('testDiv').query() + const childText = page.getByText('Div').query() expect(nonGroupChild).not.toBeInTheDocument() expect(childText).not.toBeInTheDocument() @@ -110,7 +108,7 @@ describe('', () => { it('elementRef should return ref to the group', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( @@ -126,7 +124,7 @@ describe('', () => { describe('renderGroupTitle', () => { it('should display', async () => { - render( + await render( @@ -135,13 +133,13 @@ describe('', () => { ) - const title = screen.getByText('Group Title') + const title = page.getByText('Group Title').element() expect(title).toBeInTheDocument() }) it('should display, if function is provided', async () => { - render( + await render( 'Group Title'}> @@ -150,7 +148,7 @@ describe('', () => { ) - const title = screen.getByText('Group Title') + const title = page.getByText('Group Title').element() expect(title).toBeInTheDocument() }) @@ -158,7 +156,7 @@ describe('', () => { describe('separators', () => { it("shouldn't display, when group is first and/or last item", async () => { - const { container } = render( + const { container } = await render( @@ -173,7 +171,7 @@ describe('', () => { }) it('should display by default, if group is between other items', async () => { - const { container } = render( + const { container } = await render( Option @@ -190,7 +188,7 @@ describe('', () => { }) it("shouldn't display extra separator between groups", async () => { - const { container } = render( + const { container } = await render( Option @@ -212,7 +210,7 @@ describe('', () => { describe('disabled prop', () => { it('should disable all items in the group', async () => { - const { container } = render( + const { container } = await render( Option @@ -236,7 +234,7 @@ describe('', () => { }) it('should not allow selection if the Drilldown.Group is disabled', async () => { - render( + await render( @@ -245,8 +243,10 @@ describe('', () => { ) - const optionItemContainer = screen.getByLabelText('Disabled Option') - const optionContent = screen.getByText('Disabled Option') + const optionItemContainer = page + .getByLabelText('Disabled Option') + .element() + const optionContent = page.getByText('Disabled Option').element() expect(optionItemContainer).toHaveAttribute('aria-checked', 'false') @@ -258,7 +258,7 @@ describe('', () => { describe('role prop', () => { it('should be "group" by default', async () => { - const { container } = render( + const { container } = await render( @@ -274,7 +274,7 @@ describe('', () => { }) it('should render the group with passed role', async () => { - const { container } = render( + const { container } = await render( @@ -292,7 +292,7 @@ describe('', () => { describe('as prop', () => { it('should inherit Drilldown\'s "ul" by default', async () => { - const { container } = render( + const { container } = await render( @@ -312,7 +312,7 @@ describe('', () => { }) it('should inherit Drilldown\'s "as" prop', async () => { - const { container } = render( + const { container } = await render( @@ -332,7 +332,7 @@ describe('', () => { }) it('should render the group as other element', async () => { - const { container } = render( + const { container } = await render( @@ -354,7 +354,7 @@ describe('', () => { describe('selectableType', () => { it('if not set, should render role="menuitem" options without icon by default', async () => { - const { container } = render( + const { container } = await render( @@ -371,7 +371,7 @@ describe('', () => { }) it('value "single" should render role="menuitemradio" options with icon', async () => { - const { container } = render( + const { container } = await render( @@ -388,7 +388,7 @@ describe('', () => { }) it('value "multiple" should render role="menuitemcheckbox" options with icon', async () => { - const { container } = render( + const { container } = await render( @@ -408,7 +408,7 @@ describe('', () => { describe('defaultSelected', () => { describe('if not provided', () => { it('all group options has to be unselected', async () => { - render( + await render( @@ -425,7 +425,7 @@ describe('', () => { ) - const options = screen.getAllByRole('menuitemcheckbox') + const options = page.getByRole('menuitemcheckbox').elements() expect(options.length).toBe(3) options.forEach((option) => { @@ -434,7 +434,7 @@ describe('', () => { }) it('all checkboxes should not be visible', async () => { - const { container } = render( + const { container } = await render( @@ -464,7 +464,7 @@ describe('', () => { describe('if provided', () => { it('all selected checkboxes should be visible', async () => { const selectedValues = ['item1', 'item3'] - const { container } = render( + const { container } = await render( ', () => { ) - const options = screen.getAllByRole('menuitemcheckbox') + const options = page.getByRole('menuitemcheckbox').elements() const icons = container.querySelectorAll('svg[name="Check"]') expect(options[0]).toHaveAttribute('aria-checked', 'true') @@ -499,7 +499,7 @@ describe('', () => { it("should be overridden by the Option's own defaultSelected", async () => { const selectedValues = ['item1', 'item3'] - render( + await render( ', () => { ) - const options = screen.getAllByRole('menuitemcheckbox') + const options = page.getByRole('menuitemcheckbox').elements() expect(options[0]).toHaveAttribute('aria-checked', 'true') // from the group, expect(options[1]).toHaveAttribute('aria-checked', 'true') // from own prop, @@ -538,7 +538,7 @@ describe('', () => { describe('for "single" selectableType', () => { it('the selected option should be checked', async () => { const selectedValues = ['item2'] - render( + await render( ', () => { ) - const options = screen.getAllByRole('menuitemradio') + const options = page.getByRole('menuitemradio').elements() expect(options[0]).toHaveAttribute('aria-checked', 'false') expect(options[1]).toHaveAttribute('aria-checked', 'true') @@ -568,7 +568,7 @@ describe('', () => { it("should throw error if multiple items are selected, and shouldn't select any item", async () => { const selectedValues = ['item1', 'item3'] - render( + await render( ', () => { ) - const options = screen.getAllByRole('menuitemradio') + const options = page.getByRole('menuitemradio').elements() expect(options[0]).toHaveAttribute('aria-checked', 'false') expect(options[1]).toHaveAttribute('aria-checked', 'false') @@ -604,7 +604,7 @@ describe('', () => { describe('selection', () => { it('should fire "onSelect" callback', async () => { const onSelect = vi.fn() - render( + await render( ', () => { ) - const option2 = screen.getByText('Option 2') + const option2 = page.getByText('Option 2').element() await userEvent.click(option2) - await waitFor(() => { + await vi.waitFor(() => { expect(onSelect).toHaveBeenCalledTimes(1) const args = onSelect.mock.calls[0][1] @@ -652,8 +652,8 @@ describe('', () => { }) describe('selectedOptions', () => { - it('should warn if selectableType="single" but multiple values are passed', () => { - render( + it('should warn if selectableType="single" but multiple values are passed', async () => { + await render( ', () => { }) it('should prevent user selection if selectedOptions provided', async () => { - render( + await render( ', () => { ) - const options = screen.getAllByRole('menuitemcheckbox') + const options = page.getByRole('menuitemcheckbox').elements() expect(options[0]).toHaveAttribute('aria-checked', 'true') expect(options[1]).toHaveAttribute('aria-checked', 'false') - await userEvent.click(screen.getByText('C2')) + await userEvent.click(page.getByText('C2').element()) expect(options[0]).toHaveAttribute('aria-checked', 'true') expect(options[1]).toHaveAttribute('aria-checked', 'false') }) it('should preserve user changes in uncontrolled groups when selectedOptions props change', async () => { - const { rerender } = render( + const { rerender } = await render( ', () => { ) - const options = screen.getAllByRole('menuitemcheckbox') + const options = page.getByRole('menuitemcheckbox').elements() expect(options[0]).toHaveAttribute('aria-checked', 'true') // C1 controlled expect(options[1]).toHaveAttribute('aria-checked', 'false') // C2 controlled @@ -749,14 +749,14 @@ describe('', () => { expect(options[3]).toHaveAttribute('aria-checked', 'false') // D2 uncontrolled // user changes uncontrolled group (select D2) - await userEvent.click(screen.getByText('D2')) + await userEvent.click(page.getByText('D2').element()) expect(options[0]).toHaveAttribute('aria-checked', 'true') // C1 controlled expect(options[1]).toHaveAttribute('aria-checked', 'false') // C2 controlled expect(options[2]).toHaveAttribute('aria-checked', 'true') // D1 uncontrolled expect(options[3]).toHaveAttribute('aria-checked', 'true') // D2 uncontrolled // controlled selectedOptions prop changes - rerender( + await rerender( ', () => { ) } - it('should set correctly when option in selectedOptions prop, in group default prop, option default=false', () => { - renderSelectionTest({ + it('should set correctly when option in selectedOptions prop, in group default prop, option default=false', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['val1'], defaultSelected: ['val1'] }, optionProps: { value: 'val1', defaultSelected: false } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'true' ) // selectedOptions prop wins }) - it('should set correctly when option in selectedOptions prop, not in group default prop, option default=false', () => { - renderSelectionTest({ + it('should set correctly when option in selectedOptions prop, not in group default prop, option default=false', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['val1'], defaultSelected: ['x'] }, optionProps: { value: 'val1', defaultSelected: false } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'true' ) // selectedOptions prop wins }) - it('should set correctly when option not in selectedOptions prop, in group default prop, option default=false', () => { - renderSelectionTest({ + it('should set correctly when option not in selectedOptions prop, in group default prop, option default=false', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['x'], defaultSelected: ['val1'] }, optionProps: { value: 'val1', defaultSelected: false } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'false' ) // selectedOptions prop wins }) - it('should set correctly when option not in selectedOptions prop, not in group default prop, option default=true', () => { - renderSelectionTest({ + it('should set correctly when option not in selectedOptions prop, not in group default prop, option default=true', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['x'], defaultSelected: ['x'] }, optionProps: { value: 'val1', defaultSelected: true } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'false' ) // selectedOptions prop wins }) - it('should set correctly when option controlled prop not exist, in group default prop, option default=false)', () => { - renderSelectionTest({ + it('should set correctly when option controlled prop not exist, in group default prop, option default=false)', async () => { + await renderSelectionTest({ groupProps: { defaultSelected: ['val1'] }, optionProps: { value: 'val1', defaultSelected: false } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'false' ) // option default wins }) - it('should set correctly when option controlled prop not exist, not in group default prop, option default=true', () => { - renderSelectionTest({ + it('should set correctly when option controlled prop not exist, not in group default prop, option default=true', async () => { + await renderSelectionTest({ groupProps: { defaultSelected: ['x'] }, optionProps: { value: 'val1', defaultSelected: true } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'true' ) // option default wins }) - it('should set correctly when option in selectedOptions prop, group default not exist, option default=false', () => { - renderSelectionTest({ + it('should set correctly when option in selectedOptions prop, group default not exist, option default=false', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['val1'] }, optionProps: { value: 'val1', defaultSelected: false } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'true' ) // selectedOptions prop wins }) - it('should set correctly when option not in selectedOptions prop, group default not exist, option default=true', () => { - renderSelectionTest({ + it('should set correctly when option not in selectedOptions prop, group default not exist, option default=true', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['x'] }, optionProps: { value: 'val1', defaultSelected: true } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'false' ) // selectedOptions prop wins }) - it('should set correctly when option in selectedOptions prop, not in group default prop, option default not exist', () => { - renderSelectionTest({ + it('should set correctly when option in selectedOptions prop, not in group default prop, option default not exist', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['val1'], defaultSelected: ['x'] }, optionProps: { value: 'val1' } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'true' ) // selectedOptions prop wins }) - it('should set correctly when option not in selectedOptions prop, in group default prop, option default not exist', () => { - renderSelectionTest({ + it('should set correctly when option not in selectedOptions prop, in group default prop, option default not exist', async () => { + await renderSelectionTest({ groupProps: { selectedOptions: ['x'], defaultSelected: ['val1'] }, optionProps: { value: 'val1' } }) - expect(screen.getByTestId(TEST_ID)).toHaveAttribute( + expect(page.getByTestId(TEST_ID).element()).toHaveAttribute( 'aria-checked', 'false' ) // selectedOptions prop wins @@ -997,8 +997,8 @@ describe('', () => { ) - it('should overwrite previous selectedOptions prop selections', () => { - const { rerender } = render( + it('should overwrite previous selectedOptions prop selections', async () => { + const { rerender } = await render( getDrilldownComponent({ groupProps: { selectedOptions: ['val1', 'val2'] @@ -1009,20 +1009,20 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'true' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'true' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'false' ) - rerender( + await rerender( getDrilldownComponent({ groupProps: { selectedOptions: ['val3'] @@ -1033,22 +1033,22 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'true' ) }) - it('should overwrite group default selection and option default selection', () => { - const { rerender } = render( + it('should overwrite group default selection and option default selection', async () => { + const { rerender } = await render( getDrilldownComponent({ groupProps: { selectedOptions: ['val1'], @@ -1060,21 +1060,21 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'true' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'false' ) // Set prop: selectedOptions - rerender( + await rerender( getDrilldownComponent({ groupProps: { selectedOptions: [], @@ -1086,22 +1086,22 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'false' ) }) - it('should overwrite group default selection and option default selection when selectedOptions provided', () => { - const { rerender } = render( + it('should overwrite group default selection and option default selection when selectedOptions provided', async () => { + const { rerender } = await render( getDrilldownComponent({ groupProps: { defaultSelected: ['val2'] @@ -1112,21 +1112,21 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'true' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'true' ) // Set new prop: selectedOptions - rerender( + await rerender( getDrilldownComponent({ groupProps: { selectedOptions: [], @@ -1138,15 +1138,15 @@ describe('', () => { }) ) - expect(screen.getByTestId(TEST_ID_1)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_1).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_2)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_2).element()).toHaveAttribute( 'aria-checked', 'false' ) - expect(screen.getByTestId(TEST_ID_3)).toHaveAttribute( + expect(page.getByTestId(TEST_ID_3).element()).toHaveAttribute( 'aria-checked', 'false' ) @@ -1154,4 +1154,91 @@ describe('', () => { }) }) }) + + describe('Component tests', () => { + const renderDrilldown = ( + selectableType: 'single' | 'multiple', + defaultSelected: string[] + ) => + render( + + + + + Option0 + + + Option1 + + + Option2 + + + + + ) + + it('should toggle the selected option only when selectableType is multiple', async () => { + await renderDrilldown('multiple', ['item0', 'item1', 'item2']) + + const options = page.getByRole('menuitemcheckbox').elements() + + options.forEach((option) => { + expect(option).toHaveAttribute('aria-checked', 'true') + }) + + await userEvent.click(page.getByText('Option1')) + + await vi.waitFor(() => { + expect(options[0]).toHaveAttribute('aria-checked', 'true') + expect(options[1]).toHaveAttribute('aria-checked', 'false') + expect(options[2]).toHaveAttribute('aria-checked', 'true') + }) + }) + + it('should toggle options in radio fashion when selectableType is single', async () => { + await renderDrilldown('single', ['item0']) + + const options = page.getByRole('menuitemradio').elements() + + expect(options[0]).toHaveAttribute('aria-checked', 'true') + expect(options[1]).toHaveAttribute('aria-checked', 'false') + expect(options[2]).toHaveAttribute('aria-checked', 'false') + + await userEvent.click(page.getByText('Option1')) + + await vi.waitFor(() => { + expect(options[0]).toHaveAttribute('aria-checked', 'false') + expect(options[1]).toHaveAttribute('aria-checked', 'true') + expect(options[2]).toHaveAttribute('aria-checked', 'false') + }) + }) + + it('should themeOverride prop passed to the Options component', async () => { + await render( + + + + Option + Option + + + + ) + const groupTitle = Array.from( + document.querySelectorAll('[role="presentation"]') + ).find((el) => el.textContent === 'Group label')! + + expect(groupTitle).toBeInTheDocument() + expect(getComputedStyle(groupTitle).color).toBe('rgb(100, 0, 0)') + }) + }) }) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownOption.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownOption.test.tsx index 02ea24a707..51ba7411c9 100644 --- a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownOption.test.tsx +++ b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownOption.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { CheckInstUIIcon } from '@instructure/ui-icons' @@ -51,7 +50,7 @@ describe('', () => { }) it('should allow setting "selected" property on Options', async () => { - render( + await render( @@ -65,7 +64,7 @@ describe('', () => { ) - const selectedOption = screen.getByLabelText('Option - 2') + const selectedOption = page.getByLabelText('Option - 2').element() expect(selectedOption).toBeInTheDocument() expect(selectedOption).toHaveAttribute('id', 'groupOption02') @@ -74,7 +73,7 @@ describe('', () => { describe('id prop', () => { it('should throw warning the id is not provided', async () => { - render( + await render( {/* @ts-expect-error: Testing behavior when `id` is missing */} @@ -92,7 +91,7 @@ describe('', () => { }) it('should throw warning the id is duplicated', async () => { - render( + await render( Option1 @@ -110,7 +109,7 @@ describe('', () => { }) it('should not render the options with duplicated id', async () => { - render( + await render( Option1 @@ -119,8 +118,8 @@ describe('', () => { ) - const menuitems = screen.queryAllByRole('menuitem') - const option = screen.queryByText('Option2') + const menuitems = page.getByRole('menuitem').elements() + const option = page.getByText('Option2').query() expect(menuitems.length).toBe(0) expect(option).not.toBeInTheDocument() @@ -129,7 +128,7 @@ describe('', () => { describe('children function prop', () => { it('should throw warning if it returns nothing', async () => { - render( + await render( {() => null} @@ -148,7 +147,7 @@ describe('', () => { it('should provide props as parameters', async () => { const childrenFunction = vi.fn(() => 'Option') - render( + await render( {childrenFunction} @@ -167,7 +166,7 @@ describe('', () => { describe('elementRef prop', () => { it('should give back to ref for the option wrapper (Options.Item)', async () => { const elementRef = vi.fn() - const { container } = render( + const { container } = await render( @@ -184,7 +183,7 @@ describe('', () => { describe('subPageId prop', () => { it('should display arrow icon', async () => { - const { container } = render( + const { container } = await render( @@ -200,7 +199,7 @@ describe('', () => { }) it('should indicate subpage fo SR', async () => { - render( + await render( @@ -209,7 +208,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() expect(option).toHaveAttribute('role', 'menuitem') expect(option).toHaveAttribute('aria-haspopup', 'true') @@ -218,7 +217,7 @@ describe('', () => { describe('disabled prop', () => { it('should mark option as disabled for SR', async () => { - render( + await render( @@ -227,13 +226,13 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() expect(option).toHaveAttribute('aria-disabled', 'true') }) it('should not allow selection if the Drilldown.Option itself is disabled', async () => { - render( + await render( @@ -244,8 +243,10 @@ describe('', () => { ) - const optionItemContainer = screen.getByLabelText('Disabled Option') - const optionContent = screen.getByText('Disabled Option') + const optionItemContainer = page + .getByLabelText('Disabled Option') + .element() + const optionContent = page.getByText('Disabled Option').element() expect(optionItemContainer).toHaveAttribute('aria-checked', 'false') @@ -257,7 +258,7 @@ describe('', () => { describe('href prop', () => { it('should display option as link', async () => { - render( + await render( @@ -266,14 +267,14 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() expect(option.tagName).toBe('A') expect(option).toHaveAttribute('href', '/helloWorld') }) it('should throw warning if subPageId is provided too', async () => { - render( + await render( @@ -282,7 +283,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() const expectedErrorMessage = 'Warning: Drilldown.Option with id "option1" has subPageId, so it will ignore the "href" property.' @@ -296,7 +297,7 @@ describe('', () => { }) it('should throw warning if option is in selectable group', async () => { - render( + await render( @@ -307,7 +308,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() const expectedErrorMessage = 'Warning: Drilldown.Option with id "groupOption01" is in a selectable group, so it will ignore the "href" property.' @@ -323,14 +324,14 @@ describe('', () => { describe('as prop', () => { it('should render option as `li` by default', async () => { - render( + await render( Option ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() const wrapper = option.parentElement expect(option).toHaveAttribute('id', 'option1') @@ -338,7 +339,7 @@ describe('', () => { }) it('should force option to be `li` while the parent is "ul" or "ol"', async () => { - render( + await render( @@ -347,7 +348,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() const wrapper = option.parentElement expect(option).toHaveAttribute('id', 'option1') @@ -355,7 +356,7 @@ describe('', () => { }) it('should render option as specified html element, when the parent in non-list element', async () => { - render( + await render( @@ -364,7 +365,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() const wrapper = option.parentElement expect(option).toHaveAttribute('id', 'option1') @@ -374,20 +375,20 @@ describe('', () => { describe('role prop', () => { it('should be "menuitem" by default', async () => { - render( + await render( Option ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() expect(option).toHaveAttribute('role', 'menuitem') }) it('should be applied on prop', async () => { - render( + await render( @@ -396,7 +397,7 @@ describe('', () => { ) - const option = screen.getByLabelText('Option') + const option = page.getByLabelText('Option').element() expect(option).toHaveAttribute('role', 'presentation') }) @@ -404,7 +405,7 @@ describe('', () => { describe('renderLabelInfo prop', () => { it('should display tag next to the label', async () => { - const { container } = render( + const { container } = await render( @@ -421,7 +422,7 @@ describe('', () => { it('as function should have option props as params', async () => { const infoFunction = vi.fn(() => 'Info') - render( + await render( @@ -443,7 +444,7 @@ describe('', () => { describe('renderBeforeLabel prop', () => { it('should display icon before the label', async () => { - const { container } = render( + const { container } = await render( ', () => { it('as function should have option props as params', async () => { const beforeLabelFunction = vi.fn(() => ) - render( + await render( ', () => { }) it('should throw warning if it is in selectable group', async () => { - render( + await render( @@ -512,7 +513,7 @@ describe('', () => { describe('renderAfterLabel prop', () => { it('should display icon before the label', async () => { - const { container } = render( + const { container } = await render( ', () => { it('as function should have option props as params', async () => { const beforeLabelFunction = vi.fn(() => ) - render( + await render( ', () => { }) it('should throw warning if it has subPageId', async () => { - render( + await render( ', () => { describe('description prop', () => { it('should display description under the option', async () => { - render( + await render( @@ -592,13 +593,13 @@ describe('', () => { ) - const description = screen.getByText('This is a description.') + const description = page.getByText('This is a description.').element() expect(description).toBeInTheDocument() }) it('as a function should display description under the option', async () => { - render( + await render( ', () => { ) - const description = screen.getByText('This is a description.') + const description = page.getByText('This is a description.').element() expect(description).toBeInTheDocument() }) @@ -618,7 +619,7 @@ describe('', () => { describe('descriptionRole prop', () => { it('should set the role of description', async () => { - render( + await render( ', () => { ) - const description = screen.getByText('This is a description.') + const description = page.getByText('This is a description.').element() expect(description).toHaveAttribute('role', 'button') }) @@ -640,7 +641,7 @@ describe('', () => { describe('onOptionClick callback', () => { it('should fire on click with correct params', async () => { const onOptionClick = vi.fn() - render( + await render( @@ -649,11 +650,11 @@ describe('', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { + await vi.waitFor(() => { expect(onOptionClick).toHaveBeenCalledTimes(1) const args = onOptionClick.mock.calls[0][1] @@ -670,7 +671,7 @@ describe('', () => { }) it('should provide goToPreviousPage method that throws a warning, if there is no previous page', async () => { - render( + await render( ', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { + await vi.waitFor(() => { const expectedErrorMessage = 'Warning: There is no previous page to go to. The current page history is: [page0].' @@ -701,7 +702,7 @@ describe('', () => { describe('provide goToPage method', () => { it("should throws warning if page doesn't exist", async () => { - render( + await render( ', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { + await vi.waitFor(() => { const expectedErrorMessage = 'Warning: Cannot go to page because page with id: "page1" doesn\'t exist.' @@ -731,7 +732,7 @@ describe('', () => { }) it('should throws warning if if no page id is provided', async () => { - render( + await render( ', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { + await vi.waitFor(() => { const expectedErrorMessage = 'Warning: Cannot go to page because there was no page id provided.' @@ -762,7 +763,7 @@ describe('', () => { }) it('should throws warning if parameter is not string', async () => { - render( + await render( ', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { + await vi.waitFor(() => { const expectedErrorMessage = 'Warning: Cannot go to page because parameter newPageId has to be string (valid page id). Current newPageId is "object".' @@ -793,4 +794,311 @@ describe('', () => { }) }) }) + + describe('Component tests', () => { + const optionItemWithText = (text: string) => + Array.from( + document.querySelectorAll('li[class$="-optionItem"]') + ).find((item) => item.textContent?.includes(text))! + + afterEach(() => { + // the href tests navigate via the hash, clear it so it doesn't leak + window.location.hash = '' + }) + + it('should allow controlled behaviour', async () => { + const options = ['one', 'two', 'three'] + const Example = ({ + opts, + selected + }: { + opts: typeof options + selected: string + }) => { + return ( + + + + {opts.map((opt) => { + return ( + + {opt} + + ) + })} + + + + ) + } + const { rerender } = await render( + + ) + + const menuItems = page.getByRole('menuitem').elements() + + expect(menuItems[0]).toHaveAttribute('aria-checked', 'false') + expect(menuItems[1]).toHaveAttribute('aria-checked', 'true') + expect(menuItems[2]).toHaveAttribute('aria-checked', 'false') + + await rerender() + + await vi.waitFor(() => { + const updatedMenuItems = page.getByRole('menuitem').elements() + + expect(updatedMenuItems[0]).toHaveAttribute('aria-checked', 'false') + expect(updatedMenuItems[1]).toHaveAttribute('aria-checked', 'false') + expect(updatedMenuItems[2]).toHaveAttribute('aria-checked', 'true') + }) + }) + + it('should navigate to subPage on select', async () => { + await render( + + + + Option01 + + + + Sub-Option + + + ) + expect(page.getByText('Sub-Option').query()).not.toBeInTheDocument() + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Sub-Option').element()).toBeVisible() + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + }) + }) + + it('should disabled prop apply disabled css style', async () => { + await render( + + + + Option + + + + ) + const option = document.querySelector('#option1')! + + expect(option).toHaveAttribute('aria-disabled', 'true') + expect(getComputedStyle(option).cursor).toBe('not-allowed') + }) + + it('should navigate to url on Focus + Space', async () => { + await render( + + + + Option + + + + ) + document.querySelector('#option1')!.focus() + + await userEvent.keyboard(' ') + + await vi.waitFor(() => { + expect(window.location.hash).toBe('#helloWorld') + }) + }) + + it('should navigate to url on Click', async () => { + await render( + + + + Option + + + + ) + + await userEvent.click(page.getByText('Option')) + + await vi.waitFor(() => { + expect(window.location.hash).toBe('#helloWorld') + }) + }) + + it("shouldn't navigate to url, if disabled", async () => { + await render( + + + + Option + + + + ) + + await userEvent.click(page.getByText('Option')) + + expect(window.location.hash).not.toBe('#helloWorld') + }) + + it('should renderLabelInfo prop affected by afterLabelContentVAlign prop', async () => { + await render( + + + + Option + + + + ) + const labelInfo = document.querySelector( + '[class$=-drilldown__optionLabelInfo]' + )! + + expect(labelInfo).toHaveTextContent('Info') + expect(getComputedStyle(labelInfo).alignSelf).toBe('flex-end') + }) + + it('should provide goToPreviousPage method that goes back to the previous page', async () => { + await render( + + + + Option01 + + + + + { + goToPreviousPage() + }} + > + Option11 + + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + expect(page.getByText('Option11').element()).toBeVisible() + }) + + await userEvent.click(page.getByText('Option11')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').element()).toBeVisible() + expect(page.getByText('Option11').query()).not.toBeInTheDocument() + }) + }) + + it('should provide goToPage method that can be used to go back a page', async () => { + await render( + + + + Option01 + + + + + { + goToPage(pageHistory[0]) + }} + > + Option11 + + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + expect(page.getByText('Option11').element()).toBeVisible() + }) + + await userEvent.click(page.getByText('Option11')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').element()).toBeVisible() + expect(page.getByText('Option11').query()).not.toBeInTheDocument() + }) + }) + + it('should provide goToPage method that can be used to go to a new, existing page', async () => { + await render( + + + { + goToPage('page1') + }} + > + Option01 + + + + + Option11 + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + expect(page.getByText('Option11').element()).toBeVisible() + }) + }) + + it('should themeOverride prop passed to the Options.Item component', async () => { + await render( + + + + Option01 + + + + ) + const optionItem = optionItemWithText('Option01') + const style = getComputedStyle(optionItem) + + expect(style.color).toBe('rgb(0, 0, 100)') + expect(style.backgroundColor).toBe('rgb(200, 200, 200)') + }) + }) }) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownPage.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownPage.test.tsx index 147b79d813..4d50bb052a 100644 --- a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownPage.test.tsx +++ b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownPage.test.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { render, screen, waitFor } from '@testing-library/react' -import { vi } from 'vitest' -import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { Drilldown } from '@instructure/ui-drilldown/latest' @@ -49,21 +48,21 @@ describe('', () => { }) it("shouldn't render non-DrilldownPage children", async () => { - render( + await render(
Div
) - const nonPageChild = screen.queryByText('DIV') + const nonPageChild = page.getByText('DIV').query() expect(nonPageChild).not.toBeInTheDocument() }) describe('header title', () => { it('should be displayed in the header', async () => { - const { container } = render( + const { container } = await render( Option @@ -73,27 +72,27 @@ describe('', () => { const titleContainer = container.querySelector( '[id^="DrilldownHeader-Title-Label_"]' ) - const title = screen.queryByText('HeaderTitleString') + const title = page.getByText('HeaderTitleString').query() expect(titleContainer).toBeInTheDocument() expect(title).toBeInTheDocument() }) it('should be displayed when function is passed', async () => { - render( + await render( 'HeaderTitleFunction'}> Option ) - const title = screen.queryByText('HeaderTitleFunction') + const title = page.getByText('HeaderTitleFunction').query() expect(title).toBeInTheDocument() }) it("shouldn't be displayed when function has no return value", async () => { - const { container } = render( + const { container } = await render( null}> Option @@ -112,7 +111,7 @@ describe('', () => { describe('header action label', () => { it('should be displayed in the header', async () => { - render( + await render( ', () => { ) - const actionLabel = screen.queryByText('HeaderActionLabelString') + const actionLabel = page.getByText('HeaderActionLabelString').query() expect(actionLabel).toBeInTheDocument() }) it('should be displayed when function is passed', async () => { - render( + await render( ', () => { ) - const actionLabel = screen.queryByText('HeaderActionLabelFunction') + const actionLabel = page.getByText('HeaderActionLabelFunction').query() expect(actionLabel).toBeInTheDocument() }) it("shouldn't be displayed when function has no return value", async () => { - const { container } = render( + const { container } = await render( null}> Option @@ -162,7 +161,7 @@ describe('', () => { it('should fire header action callback', async () => { const actionCallback = vi.fn() - render( + await render( ', () => { ) - const actionLabel = screen.getByText('ActionWithCallback') + const actionLabel = page.getByText('ActionWithCallback').element() await userEvent.click(actionLabel) - await waitFor(() => { + await vi.waitFor(() => { expect(actionCallback).toHaveBeenCalled() }) }) @@ -185,20 +184,20 @@ describe('', () => { describe('header back navigation', () => { it('should not be displayed on root page', async () => { - render( + await render( Option ) - const label = screen.queryByText('HeaderBackString') + const label = page.getByText('HeaderBackString').query() expect(label).not.toBeInTheDocument() }) it('should be displayed on subpages', async () => { - render( + await render( @@ -210,12 +209,12 @@ describe('', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { - const label = screen.queryByText('HeaderBackString') + await vi.waitFor(() => { + const label = page.getByText('HeaderBackString').query() expect(label).toBeInTheDocument() }) @@ -223,7 +222,7 @@ describe('', () => { it('should be displayed when function is passed, and can use former page title', async () => { const pageTitle = 'Page Title' - render( + await render( @@ -240,12 +239,12 @@ describe('', () => { ) - const option = screen.getByText('Option') + const option = page.getByText('Option').element() await userEvent.click(option) - await waitFor(() => { - const label = screen.queryByText(`Back to ${pageTitle}`) + await vi.waitFor(() => { + const label = page.getByText(`Back to ${pageTitle}`).query() expect(label).toBeInTheDocument() }) @@ -254,7 +253,7 @@ describe('', () => { describe('header separator', () => { it('should not be displayed, if no item is on the header', async () => { - const { container } = render( + const { container } = await render( Option @@ -269,7 +268,7 @@ describe('', () => { }) it('should be displayed, if there are items in the header', async () => { - const { container } = render( + const { container } = await render( Option @@ -284,7 +283,7 @@ describe('', () => { }) it('should not be displayed, if withoutHeaderSeparator is set', async () => { - const { container } = render( + const { container } = await render( Option @@ -301,14 +300,14 @@ describe('', () => { describe('disabled prop', () => { it('should make all options disabled', async () => { - render( + await render( Option ) - const allOptions = screen.getAllByRole('menuitem') + const allOptions = page.getByRole('menuitem').elements() allOptions.forEach((option) => { expect(option).toHaveAttribute('aria-disabled', 'true') @@ -316,7 +315,7 @@ describe('', () => { }) it('should not allow selection if the Drilldown.Page is disabled', async () => { - render( + await render( @@ -325,8 +324,10 @@ describe('', () => { ) - const optionItemContainer = screen.getByLabelText('Disabled Option') - const optionContent = screen.getByText('Disabled Option') + const optionItemContainer = page + .getByLabelText('Disabled Option') + .element() + const optionContent = page.getByText('Disabled Option').element() expect(optionItemContainer).toHaveAttribute('aria-checked', 'false') @@ -336,7 +337,7 @@ describe('', () => { }) it("shouldn't make header Back options disabled", async () => { - render( + await render( @@ -352,13 +353,13 @@ describe('', () => { ) - const subPageOption = screen.getByText('Option1') + const subPageOption = page.getByText('Option1').element() await userEvent.click(subPageOption) - await waitFor(() => { - const option2 = screen.getByLabelText('Option2') - const backOption = screen.getByLabelText('HeaderBackString') + await vi.waitFor(() => { + const option2 = page.getByLabelText('Option2').element() + const backOption = page.getByLabelText('HeaderBackString').element() expect(option2).toHaveAttribute('role', 'menuitem') expect(option2).toHaveAttribute('aria-disabled', 'true') @@ -368,4 +369,112 @@ describe('', () => { }) }) }) + + describe('Component tests', () => { + const optionItemWithText = (text: string) => + Array.from( + document.querySelectorAll('li[class$="-optionItem"]') + ).find((item) => item.textContent?.includes(text))! + + it('should have a back arrow in header back navigation', async () => { + await render( + + + + Option01 + + + + Option22 + + + ) + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + const backItem = optionItemWithText('HeaderBackString') + + // v2 renders the back button with a lucide ChevronLeft (was IconArrowOpenStart) + expect(backItem.querySelector('svg[name="ChevronLeft"]')).toBeVisible() + }) + }) + + it('should still display the back icon in header back navigation, even if function has no return value', async () => { + await render( + + + + Option01 + + + null}> + Option22 + + + ) + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + // v2 renders the back button with a lucide ChevronLeft (was IconArrowOpenStart) + const icon = document.querySelector( + 'div[role="menu"] svg[name="ChevronLeft"]' + ) + + expect(icon).toBeVisible() + }) + }) + + it('should fire onBackButtonClicked on header back navigation click', async () => { + const backNavCallback = vi.fn() + await render( + + + + Option01 + + + + Option22 + + + ) + + await userEvent.click(page.getByText('Option01')) + await userEvent.click(page.getByText('Back')) + + await vi.waitFor(() => { + expect(backNavCallback).toHaveBeenCalled() + }) + }) + + it('should go back one page on click', async () => { + await render( + + + + Option01 + + + + Option22 + + + ) + expect(page.getByText('Option01').element()).toBeVisible() + + await userEvent.click(page.getByText('Option01')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').query()).not.toBeInTheDocument() + }) + + await userEvent.click(page.getByText('Back')) + + await vi.waitFor(() => { + expect(page.getByText('Option01').element()).toBeVisible() + }) + }) + }) }) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownSeparator.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownSeparator.test.tsx index 12551a45b9..439d99c16a 100644 --- a/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownSeparator.test.tsx +++ b/packages/ui-drilldown/src/Drilldown/__tests__/DrilldownSeparator.test.tsx @@ -22,28 +22,29 @@ * SOFTWARE. */ -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' +import { render } from 'vitest-browser-react' +import { page } from 'vitest/browser' +import { describe, it, expect } from 'vitest' import { Drilldown } from '@instructure/ui-drilldown/latest' describe('', () => { it('should render', async () => { - render( + await render( ) - const separator = screen.getByTestId('separator1') + const separator = page.getByTestId('separator1').element() expect(separator).toBeVisible() expect(separator.getAttribute('class')).toContain('-separator') }) it('should not render children', async () => { - render( + await render( @@ -52,8 +53,28 @@ describe('', () => { ) - const separatorChild = screen.queryByText('Children') + const separatorChild = page.getByText('Children').query() expect(separatorChild).not.toBeInTheDocument() }) + + describe('Component tests', () => { + it('themeOverride prop should pass overrides to Option.Separator', async () => { + await render( + + + + + + ) + const separator = document.querySelector('#separator1')! + const style = getComputedStyle(separator) + + expect(style.height).toBe('16px') + expect(style.backgroundColor).toBe('rgb(0, 128, 0)') + }) + }) }) diff --git a/vitest.config.mts b/vitest.config.mts index 11db08eb30..713aa46c03 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -86,7 +86,6 @@ function getWorkspaceAliases() { find: `${pkgName}/src`, replacement: path.join(pkgPath, 'src') }) - } return aliases @@ -131,7 +130,12 @@ export default defineConfig({ 'packages/ui-calendar/**', 'packages/ui-checkbox/**', 'packages/ui-color-picker/**', - 'packages/ui-dialog/**' + 'packages/ui-date-input/**', + 'packages/ui-date-time-input/**', + 'packages/ui-dialog/**', + 'packages/ui-dom-utils/**', + 'packages/ui-drawer-layout/**', + 'packages/ui-drilldown/**' ], environment: 'jsdom', setupFiles: './vitest.setup.ts', @@ -176,7 +180,12 @@ export default defineConfig({ 'packages/ui-calendar/**/__tests__/**/*.test.tsx', 'packages/ui-checkbox/**/__tests__/**/*.test.tsx', 'packages/ui-color-picker/**/__tests__/**/*.test.tsx', - 'packages/ui-dialog/**/__tests__/**/*.test.tsx' + 'packages/ui-date-input/**/__tests__/**/*.test.tsx', + 'packages/ui-date-time-input/**/__tests__/**/*.test.tsx', + 'packages/ui-dialog/**/__tests__/**/*.test.tsx', + 'packages/ui-dom-utils/**/__tests__/**/*.test.tsx', + 'packages/ui-drawer-layout/**/__tests__/**/*.test.tsx', + 'packages/ui-drilldown/**/__tests__/**/*.test.tsx' ], setupFiles: './vitest.setup.browser.ts', name: { label: 'browser', color: 'green' }, @@ -190,6 +199,5 @@ export default defineConfig({ } } ] - } })