diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4355a89..ebff26a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - browser: [ChromeHeadless, FirefoxHeadless] + browser: [chromium, firefox] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - name: Checkout @@ -23,23 +23,14 @@ jobs: with: node-version: 20.x - - name: Update Brew (macOS) - if: matrix.os == 'macOS-latest' - run: brew update - - - name: Install Chrome (macOS) - if: matrix.os == 'macOS-latest' && matrix.browser == 'ChromeHeadless' - run: brew install --cask google-chrome - - - name: Install Firefox (macOS) - if: matrix.os == 'macOS-latest' && matrix.browser == 'FirefoxHeadless' - run: brew install --cask firefox - - name: Install Dependencies run: yarn bootstrap + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + - name: Run Tests - run: yarn test + run: yarn test --browser.name=${{ env.BROWSER }} --browser.headless env: BROWSER: ${{ matrix.browser }} diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 6d02115..0000000 --- a/karma.conf.js +++ /dev/null @@ -1,66 +0,0 @@ -const { rules, plugins } = require('webpack-atoms'); -const webpack = require('webpack'); - -module.exports = (config) => { - const { env } = process; - - config.set({ - frameworks: ['mocha', 'webpack', 'sinon-chai'], - - files: ['test/index.js'], - - preprocessors: { - 'test/index.js': ['webpack', 'sourcemap'], - }, - - webpack: { - mode: 'development', - module: { - rules: [ - { - ...rules.js(), - test: /\.[jt]sx?$/, - }, - ], - }, - resolve: { - symlinks: false, - extensions: ['.mjs', '.js', '.ts', '.tsx', '.json'], - fallback: { - util: require.resolve('util/'), - // for Enzyme/Cheerio - stream: require.resolve('stream-browserify'), - }, - }, - plugins: [ - plugins.define({ - __DEV__: true, - }), - new webpack.ProvidePlugin({ - process: 'process/browser', - }), - ], - devtool: 'inline-cheap-module-source-map', - }, - - reporters: ['mocha', 'coverage'], - - mochaReporter: { - output: 'autowatch', - }, - - coverageReporter: { - type: 'lcov', - dir: 'coverage', - }, - - customLaunchers: { - ChromeCi: { - base: 'Chrome', - flags: ['--no-sandbox'], - }, - }, - - browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'], - }); -}; diff --git a/package.json b/package.json index 74dc6d9..a7e62fc 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,13 @@ "build:pick": "cherry-pick --cwd=lib --input-dir=../src --cjs-dir=cjs --esm-dir=esm", "build:popper": "rollup src/popper.ts --file lib/cjs/popper.js --format cjs --name popper --plugin @rollup/plugin-node-resolve", "deploy-docs": "yarn --cwd www deploy", - "lint": "eslint www/*.js www/src src test *.js --ext .js,.ts,.tsx", + "lint": "eslint www/*.js www/src src test *.ts --ext .js,.ts,.tsx", "prepublishOnly": "yarn build", "release": "rollout", "start": "yarn --cwd www start", - "tdd": "cross-env NODE_ENV=test karma start", + "tdd": "vitest", "test": "yarn lint && yarn testonly", - "testonly": "yarn tdd --single-run" + "testonly": "yarn vitest --run" }, "lint-staged": { "*.js,*.tsx": "eslint --fix --ext .js,.ts,.tsx" @@ -98,66 +98,43 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@react-bootstrap/eslint-config": "^2.0.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@testing-library/react": "^11.2.7", - "@testing-library/user-event": "^13.5.0", - "@types/chai": "^4.3.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@testing-library/dom": "^10.3.1", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/classnames": "^2.3.1", - "@types/mocha": "^8.2.3", - "@types/react": "^17.0.47", - "@types/react-dom": "^17.0.17", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/react-transition-group": "^4.4.4", - "@types/sinon": "^10.0.11", - "@types/sinon-chai": "^3.2.8", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/browser": "^2.0.2", "babel-eslint": "^10.1.0", "babel-plugin-istanbul": "^6.1.1", "babel-preset-env-modules": "^1.0.1", - "chai": "^4.3.6", "cherry-pick": "^0.5.0", "cross-env": "^7.0.3", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", "eslint": "^7.24.0", "eslint-config-4catalyzer-typescript": "^3.2.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-mocha": "^8.1.0", "eslint-plugin-prettier": "^3.4.1", "eslint-plugin-react": "^7.30.0", "eslint-plugin-react-hooks": "^4.6.0", "gh-pages": "^3.1.0", "hookem": "^1.0.9", - "karma": "^6.4.3", - "karma-chrome-launcher": "^3.2.0", - "karma-coverage": "^2.2.1", - "karma-firefox-launcher": "^2.1.3", - "karma-mocha": "^2.0.1", - "karma-mocha-reporter": "^2.2.5", - "karma-sinon-chai": "^2.0.2", - "karma-sourcemap-loader": "^0.4.0", - "karma-webpack": "5.0.1", "lint-staged": "^10.5.4", - "mocha": "^8.3.2", + "playwright": "^1.45.1", "prettier": "^2.7.1", - "process": "^0.11.10", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-transition-group": "^4.4.1", "rimraf": "^3.0.2", - "rollup": "^3.10.1", - "simulant": "^0.2.2", - "sinon": "^13.0.1", - "sinon-chai": "^3.6.0", - "stream-browserify": "^3.0.0", + "rollup": "^4.18.1", "typescript": "^4.7.4", - "util": "^0.12.3", - "webpack": "^5.73.0", - "webpack-atoms": "^16.0.1", - "webpack-cli": "^4.1.0" + "vitest": "^2.0.2" }, "bugs": { "url": "https://github.com/react-restart/ui/issues" diff --git a/src/Dropdown.tsx b/src/Dropdown.tsx index 21bfa13..4acfb62 100644 --- a/src/Dropdown.tsx +++ b/src/Dropdown.tsx @@ -233,6 +233,7 @@ function Dropdown({ } const first = qsa(menuRef.current!, itemSelector)[0]; + if (first && first.focus) first.focus(); }); diff --git a/src/DropdownToggle.tsx b/src/DropdownToggle.tsx index 035a844..c576947 100644 --- a/src/DropdownToggle.tsx +++ b/src/DropdownToggle.tsx @@ -39,7 +39,7 @@ export function useDropdownToggle(): [ menuElement, } = useContext(DropdownContext) || {}; const handleClick = useCallback( - (e) => { + (e: Event | React.SyntheticEvent) => { toggle(!show, e); }, [show, toggle], diff --git a/src/useClickOutside.ts b/src/useClickOutside.ts index f9ef747..3c8597f 100644 --- a/src/useClickOutside.ts +++ b/src/useClickOutside.ts @@ -56,7 +56,7 @@ function useClickOutside( const waitingForTrigger = useRef(false); const handleMouseCapture = useCallback( - (e) => { + (e: any) => { const currentTarget = getRefTarget(ref); warning( diff --git a/test/.eslintrc b/test/.eslintrc index 02e83dc..6fca471 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -1,22 +1,10 @@ { - "extends": ["plugin:mocha/recommended"], - "env": { - "mocha": true - }, - "globals": { - "assert": true, - "expect": true, - "sinon": true - }, - "plugins": ["mocha"], "rules": { "no-script-url": 0, "no-unused-expressions": 0, "padded-blocks": 0, "react/no-multi-comp": 0, "react/prop-types": 0, - "mocha/no-exclusive-tests": 2, - "mocha/no-mocha-arrows": 0, "no-unused-vars": [2, { "varsIgnorePattern": "^_$" }] } } diff --git a/test/AnchorSpec.tsx b/test/AnchorSpec.tsx index 881b42a..b8c2686 100644 --- a/test/AnchorSpec.tsx +++ b/test/AnchorSpec.tsx @@ -1,6 +1,6 @@ import { render, fireEvent } from '@testing-library/react'; -import sinon from 'sinon'; -import { expect } from 'chai'; + +import { expect, describe, it, vi } from 'vitest'; import Anchor from '../src/Anchor'; @@ -8,45 +8,45 @@ describe('Anchor', () => { it('renders an anchor tag', () => { const { container } = render(); - container.firstElementChild!.tagName.should.equal('A'); + expect(container.firstElementChild!.tagName).toEqual('A'); }); it('forwards provided href', () => { const { container } = render(); - container - .firstElementChild!.getAttribute('href')! - .should.equal('http://google.com'); + expect(container.firstElementChild!.getAttribute('href')!).to.equal( + 'http://google.com', + ); }); it('ensures that a href is a hash if none provided', () => { const { container } = render(); - container.firstElementChild!.getAttribute('href')!.should.equal('#'); + expect(container.firstElementChild!.getAttribute('href')!).to.equal('#'); }); it('forwards onClick handler', () => { - const handleClick = sinon.spy(); + const handleClick = vi.fn(); const { container } = render(); fireEvent.click(container.firstChild!); - handleClick.should.have.been.calledOnce; + expect(handleClick).toHaveBeenCalledOnce(); }); it('provides onClick handler as onKeyDown handler for "space"', () => { - const handleClick = sinon.spy(); + const handleClick = vi.fn(); const { container } = render(); fireEvent.keyDown(container.firstChild!, { key: ' ' }); - handleClick.should.have.been.calledOnce; + expect(handleClick).toHaveBeenCalledOnce(); }); it('should call onKeyDown handler when href is non-trivial', () => { - const onKeyDownSpy = sinon.spy(); + const onKeyDownSpy = vi.fn(); const { container } = render( , @@ -54,11 +54,11 @@ describe('Anchor', () => { fireEvent.keyDown(container.firstChild!, { key: ' ' }); - onKeyDownSpy.should.have.been.calledOnce; + expect(onKeyDownSpy).toHaveBeenCalledOnce(); }); it('prevents default when no href is provided', () => { - const handleClick = sinon.spy(); + const handleClick = vi.fn(); const { container, rerender } = render(); @@ -68,21 +68,21 @@ describe('Anchor', () => { fireEvent.click(container.firstChild!); - expect(handleClick).to.have.been.calledTwice; - expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.true; - expect(handleClick.getCall(1).args[0].isDefaultPrevented()).to.be.true; + expect(handleClick).toHaveBeenCalledTimes(2); + expect(handleClick.mock.calls[0][0].isDefaultPrevented()).toEqual(true); + expect(handleClick.mock.calls[1][0].isDefaultPrevented()).toEqual(true); }); it('does not prevent default when href is provided', () => { - const handleClick = sinon.spy(); + const handleClick = vi.fn(); fireEvent.click( render().container .firstChild!, ); - expect(handleClick).to.have.been.calledOnce; - expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.false; + expect(handleClick).toHaveBeenCalledOnce(); + expect(handleClick.mock.calls[0][0].isDefaultPrevented()).toEqual(false); }); it('forwards provided role', () => { @@ -108,6 +108,6 @@ describe('Anchor', () => { render( , ).container.firstElementChild!.hasAttribute('role'), - ).to.be.false; + ).toEqual(false); }); }); diff --git a/test/ButtonSpec.tsx b/test/ButtonSpec.tsx index 9750867..1282208 100644 --- a/test/ButtonSpec.tsx +++ b/test/ButtonSpec.tsx @@ -1,27 +1,30 @@ import * as React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { vi, expect, describe, it } from 'vitest'; import Button from '../src/Button'; describe('); - container.firstElementChild!.tagName.should.equal('BUTTON'); + expect(container.firstElementChild!.tagName).toEqual('BUTTON'); }); it('Should have type=button by default', () => { const { container } = render(); - container.firstElementChild!.getAttribute('type')!.should.equal('button'); + expect(container.firstElementChild!.getAttribute('type')!).toEqual( + 'button', + ); }); it('Should show the type if passed one', () => { const { container } = render(); - container.firstElementChild!.getAttribute('type')!.should.equal('submit'); + expect(container.firstElementChild!.getAttribute('type')!).toEqual( + 'submit', + ); }); it('Should show the type if explicitly passed in when "as" is used', () => { @@ -31,7 +34,9 @@ describe(', ); - container.firstElementChild!.getAttribute('type')!.should.equal('submit'); + expect(container.firstElementChild!.getAttribute('type')!).toEqual( + 'submit', + ); }); it('Should not have default type=button when "as" is used', () => { @@ -48,7 +53,7 @@ describe('); + it('Should call onClick callback', async () => { + const spy = vi.fn(); + const { container } = render(); fireEvent.click(container.firstElementChild!); + + expect(spy).toHaveBeenCalled(); }); it('Should be disabled button', () => { @@ -83,7 +91,7 @@ describe(', ); - expect(container.querySelector(`a[disabled]`)).to.not.exist; + expect(container.querySelector(`a[disabled]`)).toBeNull(); const anchor = container.querySelector(`a[role="button"][aria-disabled]`)!; expect(anchor).to.exist; fireEvent.click(anchor); - expect(clickSpy).to.have.not.been.called; + expect(clickSpy).not.toHaveBeenCalled(); }); - // eslint-disable-next-line mocha/no-setup-in-describe ['#', ''].forEach((href) => { it(`should prevent default on trivial href="${href}" clicks`, () => { - const clickSpy = sinon.spy(); + const clickSpy = vi.fn(); const { getByText } = render(
@@ -118,15 +125,15 @@ describe('); - container.firstElementChild!.getAttribute('href')!.should.equal('#'); + expect(container.firstElementChild!.getAttribute('href')!).toEqual('#'); }); }); diff --git a/test/DropdownItemSpec.tsx b/test/DropdownItemSpec.tsx index f534230..71b1e21 100644 --- a/test/DropdownItemSpec.tsx +++ b/test/DropdownItemSpec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import sinon from 'sinon'; +import { vi, expect, describe, it } from 'vitest'; import DropdownItem from '../src/DropdownItem'; import SelectableContext from '../src/SelectableContext'; @@ -9,31 +9,32 @@ describe('', () => { it('should output a nav item as button', () => { const { getByText } = render(test); - getByText('test').tagName.should.equal('BUTTON'); + expect(getByText('test').tagName).toEqual('BUTTON'); }); it('should trigger onClick', () => { - const onClickSpy = sinon.spy(); + const onClickSpy = vi.fn(); const { getByText } = render( test, ); fireEvent.click(getByText('test')); - onClickSpy.should.be.called; + + expect(onClickSpy).toHaveBeenCalled(); }); it('should not trigger onClick if disabled', () => { - const onClickSpy = sinon.spy(); + const onClickSpy = vi.fn(); const { getByText } = render( test , ); fireEvent.click(getByText('test')); - onClickSpy.should.not.be.called; + expect(onClickSpy).not.toHaveBeenCalled(); }); it('should call onSelect if a key is defined', () => { - const onSelect = sinon.spy(); + const onSelect = vi.fn(); const { getByText } = render( test @@ -41,11 +42,11 @@ describe('', () => { ); fireEvent.click(getByText('test')); - onSelect.should.be.calledWith('abc'); + expect(onSelect.mock.calls.at(-1)![0]).toEqual('abc'); }); it('should not call onSelect onClick stopPropagation called', () => { - const onSelect = sinon.spy(); + const onSelect = vi.fn(); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); }; @@ -58,6 +59,6 @@ describe('', () => { ); fireEvent.click(getByText('test')); - onSelect.should.not.be.called; + expect(onSelect).not.toHaveBeenCalled(); }); }); diff --git a/test/DropdownSpec.js b/test/DropdownSpec.tsx similarity index 68% rename from test/DropdownSpec.js rename to test/DropdownSpec.tsx index b11f6ab..9dcba60 100644 --- a/test/DropdownSpec.js +++ b/test/DropdownSpec.tsx @@ -1,7 +1,7 @@ -import ReactDOM from 'react-dom'; -import { render, fireEvent } from '@testing-library/react'; -import { expect } from 'chai'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { expect, describe, it, afterEach, beforeEach, vi } from 'vitest'; +import userEvent from '@testing-library/user-event'; import Dropdown from '../src/Dropdown'; import DropdownItem from '../src/DropdownItem'; @@ -13,7 +13,7 @@ describe('', () => { popperConfig, renderMenuOnMount = true, ...props - }) => ( + }: any) => ( ', () => { {...menuProps} data-show={show} className="menu" + data-testid="menu" style={{ display: show ? 'flex' : 'none' }} /> ); @@ -40,13 +41,14 @@ describe('', () => { ); - const Toggle = (props) => ( + const Toggle = (props: any) => ( {(toggleProps) => ( @@ -170,38 +169,40 @@ describe('', () => { , ); - fireEvent.click(container.querySelector('.toggle')); + fireEvent.click(container.querySelector('.toggle')!); fireEvent.mouseDown(document.body); - closeSpy.should.have.been.calledTwice; - closeSpy.lastCall.args[0].should.equal(false); + expect(closeSpy).toHaveBeenCalledTimes(2); + expect(closeSpy.mock.calls.at(-1)![0]).to.equal(false); }); it('when focused and closed toggles open when the key "down" is pressed', () => { - const closeSpy = sinon.spy(); + const closeSpy = vi.fn(); const { container } = render(, { container: focusableContainer, }); - fireEvent.keyDown(container.querySelector('.toggle'), { key: 'ArrowDown' }); + fireEvent.keyDown(container.querySelector('.toggle')!, { + key: 'ArrowDown', + }); - closeSpy.should.have.been.calledOnce; - closeSpy.lastCall.args[0].should.equal(true); + expect(closeSpy).toHaveBeenCalledOnce(); + expect(closeSpy.mock.calls.at(-1)![0]).to.equal(true); }); it('closes when item is clicked', () => { - const onToggle = sinon.spy(); + const onToggle = vi.fn(); const root = render(); fireEvent.click(root.getByText('Item 4')); - onToggle.should.have.been.calledWith(false); + expect(onToggle.mock.calls[0][0]).toEqual(false); }); it('does not close when onToggle is controlled', () => { - const onToggle = sinon.spy(); + const onToggle = vi.fn(); const root = render(); @@ -209,7 +210,7 @@ describe('', () => { fireEvent.click(root.getByText('Item 1')); - onToggle.should.have.been.calledWith(false); + expect(onToggle.mock.calls[0][0]).toEqual(false); expect(root.container.querySelector('div[data-show="true"]')).to.exist; }); @@ -217,12 +218,9 @@ describe('', () => { it('has aria-labelledby same id as toggle button', () => { const root = render(); - root - .getByText('Toggle') - .getAttribute('id') - .should.equal( - root.container.querySelector('.menu').getAttribute('aria-labelledby'), - ); + expect(root.getByText('Toggle').getAttribute('id')).to.equal( + root.container.querySelector('.menu')!.getAttribute('aria-labelledby'), + ); }); it('has aria-haspopup when menu has role=menu and not otherwise', () => { @@ -262,8 +260,8 @@ describe('', () => { }); describe('focusable state', () => { - it('when focus should not be moved to first item when focusFirstItemOnShow is `false`', () => { - const root = render( + it('when focus should not be moved to first item when focusFirstItemOnShow is `false`', async () => { + render(
Toggle, @@ -275,83 +273,91 @@ describe('', () => { { container: focusableContainer }, ); - const toggle = root.getByText('Toggle'); + const toggle = screen.getByTestId('toggle'); toggle.focus(); - fireEvent.click(toggle); + await userEvent.click(toggle); - document.activeElement.should.equal(toggle); + expect(document.activeElement).toEqual(toggle); }); - it('when focused and closed sets focus on first menu item when the key "down" is pressed for role="menu"', (done) => { - const root = render( + it('when focused and closed sets focus on first menu item when the key "down" is pressed for role="menu"', async () => { + render(
Toggle, - Item 1 - Item 2 + Item 1 + Item 2
, { container: focusableContainer }, ); - const toggle = root.getByText('Toggle'); + const toggle = screen.getByTestId('toggle'); toggle.focus(); - fireEvent.keyDown(toggle, { key: 'ArrowDown' }); - - setTimeout(() => { - document.activeElement.should.equal(root.getByText('Item 1')); - done(); + fireEvent.keyDown(toggle, { + key: 'ArrowDown', }); + + await waitFor(() => + expect(document.activeElement).toEqual( + screen.getByRole('button', { name: 'Item 1' }), + ), + ); }); it('when focused and closed sets focus on first menu item when the focusFirstItemOnShow is true', () => { - const root = render( + render(
Toggle, - Item 1 - Item 2 + Item 1 + Item 2
, { container: focusableContainer }, ); - const toggle = root.getByText('Toggle'); + const toggle = screen.getByTestId('toggle'); toggle.focus(); - fireEvent.click(toggle); - - return Promise.resolve().then(() => { - document.activeElement.should.equal(root.getByText('Item 1')); + fireEvent.keyDown(toggle, { + key: 'ArrowDown', }); + + expect(document.activeElement).toEqual( + screen.getByRole('button', { name: 'Item 1' }), + ); }); it('when open and the key "Escape" is pressed the menu is closed and focus is returned to the button', () => { - const root = render(, { + render(, { container: focusableContainer, }); - const firstItem = root.getByText('Item 1'); + const firstItem = screen.getByRole('button', { name: 'Item 1' }); firstItem.focus(); - document.activeElement.should.equal(firstItem); - fireEvent.keyDown(firstItem, { key: 'Escape' }); + expect(document.activeElement).toEqual(firstItem); - document.activeElement.should.equal(root.getByText('Toggle')); + fireEvent.keyDown(firstItem, { + key: 'Escape', + }); + + expect(document.activeElement).toEqual(screen.getByTestId('toggle')); }); it('when open and a search input is focused and the key "Escape" is pressed the menu stays open', () => { - const toggleSpy = sinon.spy(); + const toggleSpy = vi.fn(); const root = render( Toggle, @@ -367,17 +373,17 @@ describe('', () => { const input = root.getByTestId('input'); input.focus(); - document.activeElement.should.equal(input); + expect(document.activeElement).toEqual(input); fireEvent.keyDown(input, { key: 'Escape' }); - document.activeElement.should.equal(input); + expect(document.activeElement).toEqual(input); - toggleSpy.should.not.be.called; + expect(toggleSpy).not.toHaveBeenCalled(); }); it('when open and the key "tab" is pressed the menu is closed and focus is progress to the next focusable element', () => { - const root = render( + render(
@@ -385,14 +391,14 @@ describe('', () => { { container: focusableContainer }, ); - const toggle = root.getByText('Toggle'); + const toggle = screen.getByTestId('toggle'); toggle.focus(); fireEvent.keyDown(toggle, { key: 'Tab' }); fireEvent.keyUp(toggle, { key: 'Tab' }); - toggle.getAttribute('aria-expanded').should.equal('false'); + expect(toggle.getAttribute('aria-expanded')).toEqual('false'); // simulating a tab event doesn't actually shift focus. // at least that seems to be the case according to SO. @@ -401,7 +407,7 @@ describe('', () => { }); it('should not call onToggle if the menu ref not defined and "tab" is pressed', () => { - const onToggleSpy = sinon.spy(); + const onToggleSpy = vi.fn(); const root = render( , { @@ -415,11 +421,11 @@ describe('', () => { fireEvent.keyDown(toggle, { key: 'Tab' }); fireEvent.keyUp(toggle, { key: 'Tab' }); - onToggleSpy.should.not.be.called; + expect(onToggleSpy).not.toHaveBeenCalled(); }); it('should not call onToggle if the menu is hidden and "tab" is pressed', () => { - const onToggleSpy = sinon.spy(); + const onToggleSpy = vi.fn(); const root = render(, { container: focusableContainer, }); @@ -430,12 +436,12 @@ describe('', () => { fireEvent.keyDown(toggle, { key: 'Tab' }); fireEvent.keyUp(toggle, { key: 'Tab' }); - onToggleSpy.should.not.be.called; + expect(onToggleSpy).not.toHaveBeenCalled(); }); describe('popper config', () => { - it('can add modifiers', (done) => { - const spy = sinon.spy(); + it('can add modifiers', async () => { + const spy = vi.fn(); const popper = { modifiers: [ { @@ -448,21 +454,18 @@ describe('', () => { }; render( - +
- Child Title + Toggle - - + Item 1 + Item 2
, ); - setTimeout(() => { - spy.should.have.been.calledOnce; - done(); - }); + await waitFor(() => expect(spy).toHaveBeenCalledOnce()); }); }); }); diff --git a/test/ModalManagerSpec.js b/test/ModalManagerSpec.js deleted file mode 100644 index a06a455..0000000 --- a/test/ModalManagerSpec.js +++ /dev/null @@ -1,181 +0,0 @@ -import css from 'dom-helpers/css'; -import getScrollbarSize from 'dom-helpers/scrollbarSize'; - -import ModalManager from '../src/ModalManager'; - -import { injectCss } from './helpers'; - -const createModal = () => ({ dialog: null, backdrop: null }); - -describe('ModalManager', () => { - let container, manager; - - beforeEach(() => { - manager?.reset(); - manager = new ModalManager(); - container = document.createElement('div'); - container.setAttribute('id', 'container'); - document.body.appendChild(container); - }); - - afterEach(() => { - manager?.reset(); - document.body.removeChild(container); - container = null; - manager = null; - }); - - it('should add Modal', () => { - let modal = createModal(); - - manager.add(modal); - - expect(manager.modals.length).to.equal(1); - expect(manager.modals[0]).to.equal(modal); - - expect(manager.state).to.eql({ - scrollBarWidth: 0, - style: { - overflow: '', - paddingRight: '', - }, - }); - }); - - it('should not add a modal twice', () => { - let modal = createModal(); - manager.add(modal); - manager.add(modal); - - expect(manager.modals.length).to.equal(1); - }); - - it('should add multiple modals', () => { - let modalA = createModal(); - let modalB = createModal(); - - manager.add(modalA); - manager.add(modalB); - - expect(manager.modals.length).to.equal(2); - }); - - it('should remove modal', () => { - let modalA = createModal(); - let modalB = createModal(); - - manager.add(modalA); - manager.add(modalB); - - manager.remove(modalA); - - expect(manager.modals.length).to.equal(1); - }); - - describe('container styles', () => { - beforeEach(() => { - injectCss(` - body { - padding-right: 20px; - padding-left: 20px; - overflow: scroll; - } - - #container { - height: 4000px; - } - `); - }); - - afterEach(() => injectCss.reset()); - - it('should set container overflow to hidden ', () => { - let modal = createModal(); - - expect(document.body.style.overflow).to.equal(''); - - manager.add(modal); - - expect(document.body.style.overflow).to.equal('hidden'); - }); - - it('should respect handleContainerOverflow', () => { - let modal = createModal(); - - expect(document.body.style.overflow).to.equal(''); - - const modalManager = new ModalManager({ handleContainerOverflow: false }); - modalManager.add(modal); - - expect(document.body.style.overflow).to.equal(''); - - modalManager.remove(modal); - - expect(document.body.style.overflow).to.equal(''); - }); - - it('should set add to existing container padding', () => { - let modal = createModal(); - manager.add(modal); - - expect(document.body.style.paddingRight).to.equal( - `${getScrollbarSize() + 20}px`, - ); - }); - - it('should set padding to left side if RTL', () => { - let modal = createModal(); - - new ModalManager({ isRTL: true }).add(modal); - - expect(document.body.style.paddingLeft).to.equal( - `${getScrollbarSize() + 20}px`, - ); - }); - - it('should restore container overflow style', () => { - let modal = createModal(); - - document.body.style.overflow = 'scroll'; - - expect(document.body.style.overflow).to.equal('scroll'); - - manager.add(modal); - manager.remove(modal); - - expect(document.body.style.overflow).to.equal('scroll'); - document.body.style.overflow = ''; - }); - - it('should reset overflow style to the computed one', () => { - let modal = createModal(); - - expect(css(document.body, 'overflow')).to.equal('scroll'); - - manager.add(modal); - manager.remove(modal); - - expect(document.body.style.overflow).to.equal(''); - expect(css(document.body, 'overflow')).to.equal('scroll'); - }); - - it('should only remove styles when there are no associated modals', () => { - let modalA = createModal(); - let modalB = createModal(); - - expect(document.body.style.overflow).to.equal(''); - - manager.add(modalA); - manager.add(modalB); - - manager.remove(modalB); - - expect(document.body.style.overflow).to.equal('hidden'); - - manager.remove(modalA); - - expect(document.body.style.overflow).to.equal(''); - expect(document.body.style.paddingRight).to.equal(''); - }); - }); -}); diff --git a/test/ModalManagerSpec.ts b/test/ModalManagerSpec.ts new file mode 100644 index 0000000..1220c3f --- /dev/null +++ b/test/ModalManagerSpec.ts @@ -0,0 +1,184 @@ +import css from 'dom-helpers/css'; +import { expect, describe, it, beforeEach, afterEach, vi } from 'vitest'; +import { waitFor } from '@testing-library/react'; +import ModalManager, { ModalInstance } from '../src/ModalManager'; + +import { injectCss } from './helpers'; + +vi.mock('../src/getScrollbarWidth', () => ({ + default: () => 10, +})); + +const createModal = (): ModalInstance => + ({ dialog: null, backdrop: null } as any); + +describe('ModalManager', () => { + let container: HTMLElement, manager: ModalManager; + + beforeEach(() => { + manager?.reset(); + manager = new ModalManager(); + container = document.createElement('div'); + container.setAttribute('id', 'container'); + document.body.appendChild(container); + }); + + afterEach(() => { + manager?.reset(); + document.body.removeChild(container); + }); + + it('should add Modal', () => { + const modal = createModal(); + + manager.add(modal); + + expect(manager.modals.length).toEqual(1); + expect(manager.modals[0]).toEqual(modal); + + expect((manager as any).state).to.eql({ + scrollBarWidth: 10, + style: { + overflow: '', + paddingRight: '', + }, + }); + }); + + it('should not add a modal twice', () => { + const modal = createModal(); + manager.add(modal); + manager.add(modal); + + expect(manager.modals.length).toEqual(1); + }); + + it('should add multiple modals', () => { + const modalA = createModal(); + const modalB = createModal(); + + manager.add(modalA); + manager.add(modalB); + + expect(manager.modals.length).toEqual(2); + }); + + it('should remove modal', () => { + const modalA = createModal(); + const modalB = createModal(); + + manager.add(modalA); + manager.add(modalB); + + manager.remove(modalA); + + expect(manager.modals.length).toEqual(1); + }); + + describe('container styles', () => { + beforeEach(() => { + injectCss(` + body { + padding-right: 20px; + padding-left: 20px; + overflow: scroll; + } + + #container { + height: 4000px; + } + `); + }); + + afterEach(() => injectCss.reset()); + + it('should set container overflow to hidden ', () => { + const modal = createModal(); + + expect(document.body.style.overflow).toEqual(''); + + manager.add(modal); + + expect(document.body.style.overflow).toEqual('hidden'); + }); + + it('should respect handleContainerOverflow', () => { + const modal = createModal(); + + expect(document.body.style.overflow).toEqual(''); + + const modalManager = new ModalManager({ handleContainerOverflow: false }); + modalManager.add(modal); + + expect(document.body.style.overflow).toEqual(''); + + modalManager.remove(modal); + + expect(document.body.style.overflow).toEqual(''); + }); + + it('should set add to existing container padding', async () => { + const modal = createModal(); + manager.add(modal); + + await waitFor(() => + expect(document.body.style.paddingRight).toEqual(`${10 + 20}px`), + ); + }); + + it('should set padding to left side if RTL', async () => { + const modal = createModal(); + + new ModalManager({ isRTL: true }).add(modal); + + await waitFor(() => + expect(document.body.style.paddingLeft).toEqual(`${10 + 20}px`), + ); + }); + + it('should restore container overflow style', () => { + const modal = createModal(); + + document.body.style.overflow = 'scroll'; + + expect(document.body.style.overflow).toEqual('scroll'); + + manager.add(modal); + manager.remove(modal); + + expect(document.body.style.overflow).toEqual('scroll'); + document.body.style.overflow = ''; + }); + + it('should reset overflow style to the computed one', () => { + const modal = createModal(); + + expect(css(document.body, 'overflow')).toEqual('scroll'); + + manager.add(modal); + manager.remove(modal); + + expect(document.body.style.overflow).toEqual(''); + expect(css(document.body, 'overflow')).toEqual('scroll'); + }); + + it('should only remove styles when there are no associated modals', () => { + const modalA = createModal(); + const modalB = createModal(); + + expect(document.body.style.overflow).toEqual(''); + + manager.add(modalA); + manager.add(modalB); + + manager.remove(modalB); + + expect(document.body.style.overflow).toEqual('hidden'); + + manager.remove(modalA); + + expect(document.body.style.overflow).toEqual(''); + expect(document.body.style.paddingRight).toEqual(''); + }); + }); +}); diff --git a/test/ModalSpec.js b/test/ModalSpec.js deleted file mode 100644 index 09485b9..0000000 --- a/test/ModalSpec.js +++ /dev/null @@ -1,434 +0,0 @@ -/* eslint-disable react/display-name */ - -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import { act } from 'react-dom/test-utils'; -import Transition from 'react-transition-group/Transition'; -import simulant from 'simulant'; - -import { render } from '@testing-library/react'; -import { mount } from 'enzyme'; - -import Modal from '../src/Modal'; -import { OPEN_DATA_ATTRIBUTE } from '../src/ModalManager'; - -describe('', () => { - let attachTo; - let wrapper; - - const mountWithRef = (el, options) => { - const ref = React.createRef(); - const Why = (props) => React.cloneElement(el, { ...props, ref }); - wrapper = mount(, options); - return ref; - }; - - beforeEach(() => { - attachTo = document.createElement('div'); - document.body.appendChild(attachTo); - }); - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - attachTo.remove(); - }); - - it('should render the modal content', () => { - const ref = mountWithRef( - - Message - , - { attachTo }, - ); - - expect(ref.current.dialog.querySelectorAll('strong')).to.have.lengthOf(1); - }); - - it('should disable scrolling on the modal container while open', (done) => { - const modal = React.createRef(); - - class Container extends React.Component { - ref = React.createRef(); - - state = { - modalOpen: true, - }; - - handleCloseModal = () => { - this.setState({ modalOpen: false }); - }; - - render() { - return ( -
-
} - container={this.ref} - > - Message - -
- ); - } - } - - render(, { container: attachTo }); - - setTimeout(() => { - const container = document.body; - - let backdrop = modal.current.backdrop; - - expect(container.style.overflow).to.equal('hidden'); - - backdrop.click(); - - expect(container.style.overflow).to.not.equal('hidden'); - - done(); - }); - }); - - it('should fire backdrop click callback', () => { - let onClickSpy = sinon.spy(); - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - let backdrop = ref.current.backdrop; - - backdrop.click(); - - expect(onClickSpy).to.have.been.calledOnce; - }); - - it('should close the modal when the backdrop is clicked', (done) => { - let doneOp = () => { - done(); - }; - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - let backdrop = ref.current.backdrop; - - backdrop.click(); - }); - - it('should not close the modal when the "static" backdrop is clicked', () => { - let onHideSpy = sinon.spy(); - - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - let { backdrop } = ref.current; - - backdrop.click(); - - expect(onHideSpy).to.not.have.been.called; - }); - - it('should close the modal when the esc key is pressed', (done) => { - let doneOp = () => { - done(); - }; - - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - let { backdrop } = ref.current; - - simulant.fire(backdrop, 'keydown', { code: 'Escape', keyCode: 27 }); - }); - - it('should not trigger onHide if e.preventDefault() called', () => { - const onHideSpy = sinon.spy(); - const onEscapeKeyDown = (e) => { - e.preventDefault(); - }; - - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - let { backdrop } = ref.current; - - simulant.fire(backdrop, 'keydown', { code: 'Escape', keyCode: 27 }); - expect(onHideSpy).to.not.have.been.called; - }); - - it('should add role to child', () => { - let dialog; - wrapper = mount( - - { - dialog = r; - }} - > - Message - - , - { attachTo }, - ); - - expect(dialog.getAttribute('role')).to.equal('document'); - }); - - it('should allow custom rendering', () => { - let dialog; - wrapper = mount( - ( - { - dialog = r; - }} - > - Message - - )} - />, - { attachTo }, - ); - - expect(dialog.getAttribute('role')).to.equal('group'); - }); - - it('should unbind listeners when unmounted', () => { - const { rerender } = render( -
- - Foo bar - -
, - { attachTo }, - ); - - expect(document.body.hasAttribute(OPEN_DATA_ATTRIBUTE)).to.equal(true); - - rerender(null); - - expect(document.body.hasAttribute(OPEN_DATA_ATTRIBUTE)).to.equal(false); - }); - - it('should pass transition callbacks to Transition', (done) => { - let count = 0; - let increment = () => count++; - - wrapper = mount( - } - onExit={increment} - onExiting={increment} - onExited={() => { - increment(); - expect(count).to.equal(6); - done(); - }} - onEnter={increment} - onEntering={increment} - onEntered={() => { - increment(); - wrapper.setProps({ show: false }); - }} - > - Message - , - { attachTo }, - ); - }); - - it('should fire show callback on mount', () => { - let onShowSpy = sinon.spy(); - - mount( - - Message - , - { attachTo }, - ); - - expect(onShowSpy).to.have.been.calledOnce; - }); - - it('should fire show callback on update', () => { - let onShowSpy = sinon.spy(); - wrapper = mount( - - Message - , - { attachTo }, - ); - - wrapper.setProps({ show: true }); - - expect(onShowSpy).to.have.been.calledOnce; - }); - - it('should fire onEscapeKeyDown callback on escape close', () => { - let onEscapeSpy = sinon.spy(); - - let ref = mountWithRef( - - Message - , - { attachTo }, - ); - - wrapper.setProps({ show: true }); - - act(() => { - simulant.fire(ref.current.backdrop, 'keydown', { - code: 'Escape', - keyCode: 27, - }); - }); - - expect(onEscapeSpy).to.have.been.calledOnce; - }); - - it('should accept role on the Modal', () => { - const ref = mountWithRef( - - Message - , - { attachTo }, - ); - - expect(ref.current.dialog.getAttribute('role')).to.equal('alertdialog'); - }); - - it('should accept the `aria-describedby` property on the Modal', () => { - const ref = mountWithRef( - - Message - , - { attachTo }, - ); - - expect(ref.current.dialog.getAttribute('aria-describedby')).to.equal( - 'modal-description', - ); - }); - - describe('Focused state', () => { - let focusableContainer = null; - - beforeEach(() => { - focusableContainer = document.createElement('div'); - focusableContainer.tabIndex = 0; - focusableContainer.className = 'focus-container'; - document.body.appendChild(focusableContainer); - focusableContainer.focus(); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(focusableContainer); - document.body.removeChild(focusableContainer); - }); - - it('should focus on the Modal when it is opened', () => { - expect(document.activeElement).to.equal(focusableContainer); - - wrapper = mount( - - Message - , - { attachTo: focusableContainer }, - ); - - document.activeElement.className.should.contain('modal'); - - wrapper.setProps({ show: false }); - expect(document.activeElement).to.equal(focusableContainer); - }); - - it('should not focus on the Modal when autoFocus is false', () => { - mount( - - Message - , - { attachTo: focusableContainer }, - ); - - expect(document.activeElement).to.equal(focusableContainer); - }); - - it('should not focus Modal when child has focus', () => { - expect(document.activeElement).to.equal(focusableContainer); - - mount( - -
- -
-
, - { attachTo: focusableContainer }, - ); - - let input = document.getElementsByTagName('input')[0]; - - expect(document.activeElement).to.equal(input); - }); - - it('should return focus to the modal', (done) => { - expect(document.activeElement).to.equal(focusableContainer); - - mount( - -
- -
-
, - { attachTo: focusableContainer }, - ); - - focusableContainer.focus(); - // focus reset runs in a timeout - setTimeout(() => { - document.activeElement.className.should.contain('modal'); - done(); - }, 50); - }); - - it('should not attempt to focus nonexistent children', () => { - // eslint-disable-next-line no-unused-vars - const Dialog = React.forwardRef((_, __) => null); - - mount( - - - , - { attachTo: focusableContainer }, - ); - }); - }); -}); diff --git a/test/ModalSpec.tsx b/test/ModalSpec.tsx new file mode 100644 index 0000000..6241f30 --- /dev/null +++ b/test/ModalSpec.tsx @@ -0,0 +1,335 @@ +/* eslint-disable react/display-name */ + +import * as React from 'react'; + +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { expect, vi, describe, it, beforeEach, afterEach } from 'vitest'; +import Modal, { ModalHandle } from '../src/Modal'; +import { OPEN_DATA_ATTRIBUTE } from '../src/ModalManager'; + +describe('', () => { + let attachTo: HTMLElement; + + beforeEach(() => { + attachTo = document.createElement('div'); + document.body.appendChild(attachTo); + }); + + afterEach(() => { + attachTo.remove(); + }); + + const getDialog = () => screen.getByRole('dialog'); + + it('should render the modal content', () => { + render( + + Message + , + { container: attachTo }, + ); + + expect(getDialog().querySelectorAll('strong')).toHaveLength(1); + }); + + it('should disable scrolling on the modal container while open', async () => { + const modal = React.createRef(); + + function Container() { + const [isOpen, setIsOpen] = React.useState(true); + return ( + setIsOpen(false)} + renderBackdrop={(p) =>
} + > + Message + + ); + } + + render(, { container: attachTo }); + + const modalContainer = document.body; + + const backdrop = modal.current!.backdrop; + + await waitFor(() => + expect(modalContainer.style.overflow).toEqual('hidden'), + ); + + await userEvent.click(backdrop!); + + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); + + expect(modalContainer.style.overflow).toEqual(''); + }); + + it('should fire backdrop click callback', async () => { + const onClickSpy = vi.fn(); + const modal = React.createRef(); + + render( + + Message + , + { container: attachTo }, + ); + + await userEvent.click(modal.current!.backdrop!); + + expect(onClickSpy).toHaveBeenCalledOnce(); + }); + + it('should close the modal when the backdrop is clicked', async () => { + const spy = vi.fn(); + const modal = React.createRef(); + + render( + + Message + , + { container: attachTo }, + ); + + await userEvent.click(modal.current!.backdrop!); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it('should not close the modal when the "static" backdrop is clicked', async () => { + const spy = vi.fn(); + const modal = React.createRef(); + + render( + + Message + , + { container: attachTo }, + ); + + await userEvent.click(modal.current!.backdrop!); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should close the modal when the esc key is pressed', () => { + const spy = vi.fn(); + + render( + + Message + , + { container: attachTo }, + ); + + fireEvent.keyDown(document.body, { key: 'Escape', keyCode: 27 }); + expect(spy).toHaveBeenCalled(); + }); + + it('should not trigger onHide if e.preventDefault() called', () => { + const spy = vi.fn(); + const modal = React.createRef(); + const onEscapeKeyDown = (e: any) => { + e.preventDefault(); + }; + + render( + + Message + , + { container: attachTo }, + ); + + fireEvent.keyDown(document.body, { key: 'Escape', keyCode: 27 }); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should add role to child', () => { + render( + +
Message
+
, + { container: attachTo }, + ); + + expect(getDialog().firstElementChild!.getAttribute('role')).toEqual( + 'document', + ); + }); + + it('should allow custom rendering', () => { + render( + ( +
+ Message +
+ )} + />, + { container: attachTo }, + ); + + expect(screen.getByTestId('group').getAttribute('role')).toEqual('group'); + }); + + it('should unbind listeners when unmounted', () => { + const { rerender } = render( +
+ + Foo bar + +
, + { container: attachTo }, + ); + + expect(document.body.hasAttribute(OPEN_DATA_ATTRIBUTE)).toEqual(true); + + rerender(null); + + expect(document.body.hasAttribute(OPEN_DATA_ATTRIBUTE)).toEqual(false); + }); + + it('should fire show callback on mount', () => { + const onShowSpy = vi.fn(); + + render( + + Message + , + { container: attachTo }, + ); + + expect(onShowSpy).toBeCalledTimes(1); + }); + + it('should fire show callback on update', () => { + const onShowSpy = vi.fn(); + const { rerender } = render( + + Message + , + { container: attachTo }, + ); + + rerender( + + Message + , + ); + + expect(onShowSpy).toBeCalledTimes(1); + }); + + it('should accept role on the Modal', () => { + render( + + Message + , + { container: attachTo }, + ); + + expect(screen.getByRole('alertdialog')).toBeTruthy(); + }); + + it('should accept the `aria-describedby` property on the Modal', () => { + render( + + Message + , + { container: attachTo }, + ); + + expect(getDialog().getAttribute('aria-describedby')).toEqual( + 'modal-description', + ); + }); + + describe('Focused state', () => { + let focusableContainer: HTMLElement; + + beforeEach(() => { + focusableContainer = document.createElement('div'); + focusableContainer.tabIndex = 0; + focusableContainer.className = 'focus-container'; + document.body.appendChild(focusableContainer); + focusableContainer.focus(); + }); + + afterEach(() => { + document.body.removeChild(focusableContainer); + }); + + it('should focus on the Modal when it is opened', () => { + expect(document.activeElement).toEqual(focusableContainer); + + const result = render( + + Message + , + { container: focusableContainer }, + ); + + expect(document.activeElement!.classList.contains('modal')).toBe(true); + + result.rerender( + + Message + , + ); + + expect(document.activeElement).toEqual(focusableContainer); + }); + + it('should not focus on the Modal when autoFocus is false', () => { + render( + + Message + , + { container: focusableContainer }, + ); + + expect(document.activeElement).toEqual(focusableContainer); + }); + + it('should not focus Modal when child has focus', () => { + expect(document.activeElement).toEqual(focusableContainer); + + render( + +
+ +
+
, + { container: focusableContainer }, + ); + + const input = document.getElementsByTagName('input')[0]; + + expect(document.activeElement).toEqual(input); + }); + + it('should return focus to the modal', async () => { + expect(document.activeElement).toEqual(focusableContainer); + + render( + +
+ +
+
, + { container: focusableContainer }, + ); + + focusableContainer.focus(); + + await waitFor(() => { + expect(document.activeElement!.classList.contains('modal')).toBe(true); + }); + }); + }); +}); diff --git a/test/NavItemSpec.tsx b/test/NavItemSpec.tsx index 6efce82..4b3d40d 100644 --- a/test/NavItemSpec.tsx +++ b/test/NavItemSpec.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { expect, describe, it, vi } from 'vitest'; import NavContext from '../src/NavContext'; import NavItem from '../src/NavItem'; @@ -11,12 +10,12 @@ describe('', () => { it('should output a nav item as button', () => { const { getByText } = render(test); - getByText('test').tagName.should.equal('BUTTON'); + expect(getByText('test').tagName).toEqual('BUTTON'); }); it('should output custom role', () => { const { getByRole } = render(test); - getByRole('abc').should.exist; + expect(getByRole('abc')).toBeTruthy(); }); it('should set role to tab if inside nav context', () => { @@ -25,15 +24,15 @@ describe('', () => { value={{ role: 'tablist', activeKey: 'key', - getControlledId: sinon.spy(), - getControllerId: sinon.spy(), + getControlledId: vi.fn(), + getControllerId: vi.fn(), }} > test , ); - getByRole('tab').should.exist; + expect(getByRole('tab')).toBeTruthy(); }); it('should not override custom role if inside nav context', () => { @@ -42,15 +41,15 @@ describe('', () => { value={{ role: 'tablist', activeKey: 'key', - getControlledId: sinon.spy(), - getControllerId: sinon.spy(), + getControlledId: vi.fn(), + getControllerId: vi.fn(), }} > test , ); - getByRole('abc').should.exist; + expect(getByRole('abc')).toBeTruthy(); }); it('should use active from nav context', () => { @@ -59,8 +58,8 @@ describe('', () => { value={{ role: 'tablist', activeKey: 'key', - getControlledId: sinon.spy(), - getControllerId: sinon.spy(), + getControlledId: vi.fn(), + getControllerId: vi.fn(), }} > test @@ -80,18 +79,18 @@ describe('', () => { ); const node = getByText('test'); expect(node.getAttribute('aria-disabled')).to.equal('true'); - node.tabIndex.should.equal(-1); + expect(node.tabIndex).toEqual(-1); }); it('should trigger onClick', () => { - const onClickSpy = sinon.spy(); + const onClickSpy = vi.fn(); const { getByText } = render(test); fireEvent.click(getByText('test')); - onClickSpy.should.be.called; + expect(onClickSpy).toHaveBeenCalled(); }); it('should not trigger onClick if disabled', () => { - const onClickSpy = sinon.spy(); + const onClickSpy = vi.fn(); const { getByText } = render( // Render as div because onClick won't get triggered with Button when disabled. @@ -99,11 +98,11 @@ describe('', () => { , ); fireEvent.click(getByText('test')); - onClickSpy.should.not.be.called; + expect(onClickSpy).not.toHaveBeenCalled(); }); it('should call onSelect if a key is defined', () => { - const onSelect = sinon.spy(); + const onSelect = vi.fn(); const { getByText } = render( test @@ -111,11 +110,11 @@ describe('', () => { ); fireEvent.click(getByText('test')); - onSelect.should.be.calledWith('abc'); + expect(onSelect.mock.calls[0][0]).toEqual('abc'); }); it('should not call onSelect onClick stopPropagation called', () => { - const onSelect = sinon.spy(); + const onSelect = vi.fn(); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); }; @@ -128,6 +127,6 @@ describe('', () => { ); fireEvent.click(getByText('test')); - onSelect.should.not.be.called; + expect(onSelect).not.toHaveBeenCalled(); }); }); diff --git a/test/NavSpec.js b/test/NavSpec.tsx similarity index 65% rename from test/NavSpec.js rename to test/NavSpec.tsx index 4cb88a6..b726f88 100644 --- a/test/NavSpec.js +++ b/test/NavSpec.tsx @@ -1,12 +1,11 @@ -/* eslint-disable mocha/no-hooks-for-single-case */ - -import { mount } from 'enzyme'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect, describe, beforeEach, afterEach, it } from 'vitest'; import Tabs from '../src/Tabs'; import Nav from '../src/Nav'; import NavItem from '../src/NavItem'; describe('