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/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/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/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-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/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-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-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/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__" diff --git a/vitest.config.mts b/vitest.config.mts index 301659c505..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 @@ -124,7 +123,19 @@ export default defineConfig({ 'packages/ui-alerts/**', 'packages/ui-avatar/**', 'packages/ui-badge/**', - 'packages/ui-dialog/**' + 'packages/ui-billboard/**', + 'packages/ui-breadcrumb/**', + 'packages/ui-buttons/**', + 'packages/ui-byline/**', + 'packages/ui-calendar/**', + 'packages/ui-checkbox/**', + 'packages/ui-color-picker/**', + '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', @@ -162,7 +173,19 @@ export default defineConfig({ 'packages/ui-alerts/**/__tests__/**/*.test.tsx', 'packages/ui-avatar/**/__tests__/**/*.test.tsx', 'packages/ui-badge/**/__tests__/**/*.test.tsx', - 'packages/ui-dialog/**/__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-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' }, @@ -176,6 +199,5 @@ export default defineConfig({ } } ] - } })