diff --git a/.editorconfig b/.editorconfig index 53b061a8..c6c8b362 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file +insert_final_newline = true diff --git a/.eslintrc.js b/.eslintrc.js index acb83293..0e331129 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,8 @@ module.exports = { + parser: 'vue-eslint-parser', parserOptions: { parser: '@typescript-eslint/parser', + sourceType: 'module', }, extends: [ './node_modules/kcd-scripts/eslint.js', @@ -8,7 +10,6 @@ module.exports = { 'plugin:testing-library/vue', 'prettier', ], - plugins: ['vue'], rules: { 'no-console': 'off', 'import/no-unresolved': 'off', diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 67baf8cb..e376fd84 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,7 +9,7 @@ assignees: '' -[![Build Status][build-badge]][build] [![Coverage Status][coverage-badge]][coverage] [![GitHub version][github-badge]][github] [![npm version][npm-badge]][npm] @@ -66,11 +65,18 @@ npm install --save-dev @testing-library/vue ``` This library has `peerDependencies` listings for `Vue 3` and -`vue-template-compiler`. +`@vue/compiler-sfc`. You may also be interested in installing `jest-dom` so you can use [the custom Jest matchers][jest-dom]. +If you're using Vue 2, please install version 5 of the library: + +``` +npm install --save-dev @testing-library/vue@^5 +``` + + ## A basic example ```html @@ -122,7 +128,7 @@ test('increments value on click', async () => { > You might want to install [`jest-dom`][jest-dom] to add handy assertions such > as `.toBeInTheDocument()`. In the example above, you could write -> `expect(screen.queryByText('Times clicked: 0')).toBeInTheDocument()`. +> `expect(screen.getByText('Times clicked: 0')).toBeInTheDocument()`. > Using `byText` queries it's not the only nor the best way to query for > elements. Read [Which query should I use?][which-query] to discover @@ -236,8 +242,6 @@ instead of filing an issue on GitHub. [![ITenthusiasm](https://avatars2.githubusercontent.com/u/47364027?v3&s=120)](https://github.com/ITenthusiasm) -[build-badge]: https://img.shields.io/github/workflow/status/testing-library/vue-testing-library/validate?logo=github -[build]: https://github.com/testing-library/vue-testing-library/actions?query=workflow%3Avalidate [coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/vue-testing-library.svg [coverage]: https://codecov.io/github/testing-library/vue-testing-library [github-badge]: https://badge.fury.io/gh/testing-library%2Fvue-testing-library.svg @@ -261,11 +265,11 @@ instead of filing an issue on GitHub. [add-issue-bug]: https://github.com/testing-library/vue-testing-library/issues/new?assignees=&labels=bug&template=bug_report.md&title= [add-issue]: (https://github.com/testing-library/vue-testing-library/issues/new) -[types-directory]: https://github.com/testing-library/vue-testing-library/blob/master/types -[test-directory]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__ -[vuex-example]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__/vuex.js -[vue-router-example]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__/vue-router.js -[vee-validate-example]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__/validate-plugin.js -[vue-i18n-example]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__/vue-i18n.js -[vuetify-example]: https://github.com/testing-library/vue-testing-library/blob/master/src/__tests__/vuetify.js +[types-directory]: https://github.com/testing-library/vue-testing-library/blob/main/types +[test-directory]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__ +[vuex-example]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/vuex.js +[vue-router-example]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/vue-router.js +[vee-validate-example]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/validate-plugin.js +[vue-i18n-example]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/translations-vue-i18n.js +[vuetify-example]: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/vuetify.js diff --git a/babel.config.js b/babel.config.js index 424f157b..31ac5180 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,11 @@ module.exports = { sourceType: 'module', + plugins: [ + // Fixes for loose issue from https://github.com/rails/webpacker/issues/3008 + ['@babel/plugin-proposal-class-properties', {loose: true}], + ['@babel/plugin-proposal-private-methods', {loose: true}], + ['@babel/plugin-proposal-private-property-in-object', {loose: true}], + ], presets: [ [ '@babel/preset-env', diff --git a/jest.config.js b/jest.config.js index 9d901b60..a440b667 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,12 +3,15 @@ const config = require('kcd-scripts/jest') module.exports = merge(config, { testEnvironment: 'jsdom', + testEnvironmentOptions: { + customExportConditions: ['node', 'node-addons'], + }, moduleFileExtensions: ['js', 'vue'], coverageDirectory: './coverage', collectCoverageFrom: ['**/src/**/*.js', '!**/src/__tests__/**'], transform: { '^.+\\.js$': '/node_modules/babel-jest', - '.*\\.(vue)$': '/node_modules/vue-jest', + '^.+\\.vue$': '@vue/vue3-jest', }, snapshotSerializers: ['/node_modules/jest-serializer-vue'], testPathIgnorePatterns: [ diff --git a/package.json b/package.json index 7c98a8e9..a3b7b084 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "setup": "npm install && npm run validate -s" }, "engines": { - "node": ">=12" + "node": ">=14" }, "files": [ "types", @@ -43,44 +43,50 @@ "author": "Daniel Cook", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.15.4", - "@testing-library/dom": "^8.5.0", - "@vue/test-utils": "^2.0.0-rc.18" + "@babel/runtime": "^7.23.2", + "@testing-library/dom": "^9.3.3", + "@vue/test-utils": "^2.4.1" }, "devDependencies": { - "@apollo/client": "^3.4.11", - "@babel/plugin-transform-runtime": "^7.15.0", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/user-event": "^13.2.1", - "@types/estree": "^0.0.50", - "@vue/apollo-composable": "^4.0.0-alpha.14", - "@vue/compiler-sfc": "^3.2.12", - "apollo-boost": "^0.4.9", - "axios": "^0.20.0", - "element-plus": "^1.3.0-beta.1", - "eslint-plugin-vue": "^8.2.0", - "graphql": "^15.5.3", - "graphql-tag": "^2.12.4", - "isomorphic-unfetch": "^3.1.0", - "jest-serializer-vue": "^2.0.2", - "kcd-scripts": "^10.0.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-runtime": "^7.23.2", + "@element-plus/icons-vue": "^2.1.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/user-event": "^14.5.1", + "@types/estree": "^1.0.3", + "@vue/compiler-sfc": "^3.3.5", + "@vue/server-renderer": "^3.3.5", + "@vue/vue3-jest": "^29.2.6", + "axios": "^1.5.1", + "element-plus": "^2.4.1", + "eslint-plugin-vue": "^9.17.0", + "isomorphic-unfetch": "^4.0.2", + "jest-environment-jsdom": "^29.7.0", + "jest-serializer-vue": "^3.1.0", + "kcd-scripts": "^14.0.0", "lodash.merge": "^4.6.2", - "msw": "^0.21.3", - "tsd": "^0.19.1", - "typescript": "^4.4.3", - "vee-validate": "^4.3.5", - "vue": "^3.2.12", - "vue-apollo": "^3.0.5", - "vue-i18n": "^9.2.0-beta.26", - "vue-jest": "^5.0.0-alpha.10", - "vue-router": "^4.0.3", - "vuetify": "^v3.0.0-alpha.12", - "vuex": "^4.0.0" + "msw": "^1.3.2", + "tsd": "^0.29.0", + "type-fest": "~2.19", + "typescript": "^5.2.2", + "vee-validate": "^4.11.8", + "vue": "^3.3.5", + "vue-component-type-helpers": "^2.0.19", + "vue-eslint-parser": "^9.3.2", + "vue-i18n": "^9.5.0", + "vue-router": "^4.2.5", + "vuex": "^4.1.0" }, "peerDependencies": { "@vue/compiler-sfc": ">= 3", "vue": ">= 3" }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + }, "husky": { "hooks": { "pre-commit": "kcd-scripts pre-commit" diff --git a/src/__tests__/simple-button.js b/src/__tests__/button-simple.js similarity index 100% rename from src/__tests__/simple-button.js rename to src/__tests__/button-simple.js diff --git a/src/__tests__/slots.js b/src/__tests__/card-slots.js similarity index 100% rename from src/__tests__/slots.js rename to src/__tests__/card-slots.js diff --git a/src/__tests__/visibility.js b/src/__tests__/collapsible-visibility.js similarity index 100% rename from src/__tests__/visibility.js rename to src/__tests__/collapsible-visibility.js diff --git a/src/__tests__/components/Form.vue b/src/__tests__/components/Form.vue index 73897cb2..c3ee7452 100644 --- a/src/__tests__/components/Form.vue +++ b/src/__tests__/components/Form.vue @@ -58,7 +58,6 @@ export default { methods: { submit() { if (this.submitDisabled) return - this.$emit('submit', { title: this.title, review: this.review, diff --git a/src/__tests__/components/NumberDisplay.vue b/src/__tests__/components/NumberDisplay.vue index 940363f0..c36dbe1c 100644 --- a/src/__tests__/components/NumberDisplay.vue +++ b/src/__tests__/components/NumberDisplay.vue @@ -1,22 +1,22 @@ - - - + + + diff --git a/src/__tests__/components/VueApollo/queries.js b/src/__tests__/components/VueApollo/queries.js index cd85fa2f..91b0b590 100644 --- a/src/__tests__/components/VueApollo/queries.js +++ b/src/__tests__/components/VueApollo/queries.js @@ -15,4 +15,4 @@ export const updateUserMutation = gql` email } } -` \ No newline at end of file +` diff --git a/src/__tests__/disappearance.js b/src/__tests__/disappearance.js index 8746f9ed..7129d565 100644 --- a/src/__tests__/disappearance.js +++ b/src/__tests__/disappearance.js @@ -11,7 +11,7 @@ test('waits for the data to be loaded', async () => { // Following line reads as follows: // "Wait until element with text 'Loading...' is gone." - await waitForElementToBeRemoved(getByText('Loading...')) + await waitForElementToBeRemoved(queryByText('Loading...')) // It is equivalent to: // // await waitFor(() => { diff --git a/src/__tests__/axios-mock.js b/src/__tests__/fetch-axios-mock.js similarity index 100% rename from src/__tests__/axios-mock.js rename to src/__tests__/fetch-axios-mock.js diff --git a/src/__tests__/form.js b/src/__tests__/form.js index 5011b359..94b460a7 100644 --- a/src/__tests__/form.js +++ b/src/__tests__/form.js @@ -62,6 +62,6 @@ test('Review form submits', async () => { // Assert the right event has been emitted. expect(emitted()).toHaveProperty('submit') - expect(emitted().submit[0][0]).toMatchObject(fakeReview) + expect(emitted('submit')[0][0]).toMatchObject(fakeReview) expect(console.warn).not.toHaveBeenCalled() }) diff --git a/src/__tests__/functional.js b/src/__tests__/functional-sfc.js similarity index 100% rename from src/__tests__/functional.js rename to src/__tests__/functional-sfc.js diff --git a/src/__tests__/debug.js b/src/__tests__/hello-world-debug.js similarity index 97% rename from src/__tests__/debug.js rename to src/__tests__/hello-world-debug.js index 554feb04..ab2adf4c 100644 --- a/src/__tests__/debug.js +++ b/src/__tests__/hello-world-debug.js @@ -1,4 +1,4 @@ -/* eslint-disable testing-library/no-debug */ +/* eslint-disable testing-library/no-debugging-utils */ import {render} from '..' import HelloWorld from './components/HelloWorld' @@ -67,7 +67,7 @@ test('allows same arguments as prettyDOM', () => { expect(console.log).toHaveBeenCalledTimes(1) expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ + [
..., ] diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 151bd8bc..235210e3 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -1,5 +1,6 @@ -import {render} from '..' +import {h, defineComponent} from 'vue' import '@testing-library/jest-dom' +import {render, cleanup} from '..' test('baseElement defaults to document.body', () => { const {baseElement} = render({template: '
'}) @@ -87,3 +88,15 @@ test('unmounts', () => { expect(queryByTestId('node')).not.toBeInTheDocument() }) + +test('unmounts when no wrapper element is present', () => { + const Comp = defineComponent((_, ctx) => () => ctx.slots.default?.()) + + const {unmount} = render({ + render: () => h(Comp, () => h('div')), + }) + + unmount() + + expect(() => cleanup()).not.toThrow() +}) diff --git a/src/__tests__/teleport.js b/src/__tests__/teleport.js index 04dbf6db..c756df12 100644 --- a/src/__tests__/teleport.js +++ b/src/__tests__/teleport.js @@ -1,5 +1,5 @@ import {defineComponent} from 'vue' -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom' import {render, fireEvent} from '..' const ModalButton = defineComponent({ diff --git a/src/__tests__/vue-i18n.js b/src/__tests__/translations-vue-i18n.js similarity index 100% rename from src/__tests__/vue-i18n.js rename to src/__tests__/translations-vue-i18n.js diff --git a/src/__tests__/user-event.js b/src/__tests__/user-event.js index 4db8c3e6..5d38af12 100644 --- a/src/__tests__/user-event.js +++ b/src/__tests__/user-event.js @@ -18,22 +18,23 @@ test('User events in a form', async () => { review: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', rating: '3', } + const user = userEvent.setup() const {getByText, getByLabelText, emitted} = render(Form) const submitButton = getByText('Submit') expect(submitButton).toBeDisabled() const titleInput = getByLabelText(/title of the movie/i) - userEvent.type(titleInput, fakeReview.title) + await user.type(titleInput, fakeReview.title) expect(titleInput).toHaveValue(fakeReview.title) const textArea = getByLabelText(/Your review/i) - userEvent.type(textArea, 'The t-rex went insane!') + await user.type(textArea, 'The t-rex went insane!') expect(textArea).toHaveValue('The t-rex went insane!') - userEvent.clear(textArea) + await user.clear(textArea) expect(textArea).toHaveValue('') - userEvent.type(textArea, fakeReview.review) + await user.type(textArea, fakeReview.review) expect(textArea).toHaveValue(fakeReview.review) const initialSelectedRating = getByLabelText(/Awful/i) @@ -41,32 +42,34 @@ test('User events in a form', async () => { expect(initialSelectedRating).toBeChecked() expect(wonderfulRadioInput).not.toBeChecked() - userEvent.click(wonderfulRadioInput) + await user.click(wonderfulRadioInput) expect(wonderfulRadioInput).toBeChecked() await waitFor(() => expect(initialSelectedRating).not.toBeChecked()) const recommendInput = getByLabelText(/Would you recommend this movie?/i) - userEvent.click(recommendInput) + await user.click(recommendInput) expect(recommendInput).toBeChecked() - userEvent.tab() + await user.tab() expect(submitButton).toHaveFocus() expect(submitButton).toBeEnabled() - userEvent.type(submitButton, '{enter}') - expect(emitted().submit[0][0]).toMatchObject(fakeReview) + await user.type(submitButton, '{enter}') + expect(emitted('submit')[0][0]).toMatchObject(fakeReview) expect(console.warn).not.toHaveBeenCalled() }) -test('selecting option with user events', () => { +test('selecting option with user events', async () => { + const user = userEvent.setup() const {getByDisplayValue} = render(Select) + const select = getByDisplayValue('Tyrannosaurus') expect(select).toHaveValue('dino1') - userEvent.selectOptions(select, 'dino2') + await user.selectOptions(select, 'dino2') expect(select).toHaveValue('dino2') - userEvent.selectOptions(select, 'dino3') + await user.selectOptions(select, 'dino3') expect(select).not.toHaveValue('dino2') expect(select).toHaveValue('dino3') }) diff --git a/src/__tests__/vue-apollo.js b/src/__tests__/vue-apollo.js deleted file mode 100644 index 6d29c517..00000000 --- a/src/__tests__/vue-apollo.js +++ /dev/null @@ -1,94 +0,0 @@ -import '@testing-library/jest-dom' -import fetch from 'isomorphic-unfetch' -import {DefaultApolloClient} from '@vue/apollo-composable' -import ApolloClient from 'apollo-boost' -import {setupServer} from 'msw/node' -import {graphql} from 'msw' -import {render, fireEvent, screen} from '..' -import Component from './components/VueApollo.vue' - -// Since vue-apollo doesn't provide a MockProvider for Vue, -// you need to use some kind of mocks for the queries. - -// We are using Mock Service Worker (aka MSW) library to declaratively mock API communication -// in your tests instead of stubbing window.fetch, or relying on third-party adapters. - -const server = setupServer( - ...[ - graphql.query('getUser', (req, res, ctx) => { - const {variables} = req - - if (variables.id !== '1') { - return res( - ctx.errors([ - { - message: 'User not found', - }, - ]), - ) - } - - return res( - ctx.data({ - user: { - id: 1, - email: 'alice@example.com', - __typename: 'User', - }, - }), - ) - }), - - graphql.mutation('updateUser', (req, res, ctx) => { - const {variables} = req - - return res( - ctx.data({ - updateUser: { - id: variables.input.id, - email: variables.input.email, - __typename: 'User', - }, - }), - ) - }), - ], -) - -beforeAll(() => server.listen()) -afterEach(() => server.resetHandlers()) -afterAll(() => server.close()) - -test('mocking queries and mutations', async () => { - const apolloClient = new ApolloClient({ - uri: 'http://localhost:3000', - fetch, - }) - - render(Component, { - props: {id: '1'}, - global: { - provide: { - [DefaultApolloClient]: apolloClient, - }, - }, - }) - - //Initial rendering will be in the loading state, - expect(screen.getByText('Loading')).toBeInTheDocument() - - expect( - await screen.findByText('Email: alice@example.com'), - ).toBeInTheDocument() - - await fireEvent.update( - screen.getByLabelText('Email'), - 'alice+new@example.com', - ) - - await fireEvent.click(screen.getByRole('button', {name: 'Change email'})) - - expect( - await screen.findByText('Email: alice+new@example.com'), - ).toBeInTheDocument() -}) diff --git a/src/__tests__/vuetify.js b/src/__tests__/vuetify.js deleted file mode 100644 index a933e8ef..00000000 --- a/src/__tests__/vuetify.js +++ /dev/null @@ -1,73 +0,0 @@ -test.todo('Your test suite must contain at least one test.') -// import '@testing-library/jest-dom' -// import Vue from 'vue' -// import {render, fireEvent} from '..' -// import Vuetify from 'vuetify' -// import VuetifyDemoComponent from './components/Vuetify' - -// // We need to use a global Vue instance, otherwise Vuetify will complain about -// // read-only attributes. -// // This could also be done in a custom Jest-test-setup file to execute for all tests. -// // More info: https://github.com/vuetifyjs/vuetify/issues/4068 -// // https://vuetifyjs.com/en/getting-started/unit-testing -// Vue.use(Vuetify) - -// // Custom container to integrate Vuetify with Vue Testing Library. -// // Vuetify requires you to wrap your app with a v-app component that provides -// // a
node. -// const renderWithVuetify = (component, options, callback) => { -// const root = document.createElement('div') -// root.setAttribute('data-app', 'true') - -// return render( -// component, -// { -// container: document.body.appendChild(root), -// // for Vuetify components that use the $vuetify instance property -// vuetify: new Vuetify(), -// ...options, -// }, -// callback, -// ) -// } - -// test('should set [data-app] attribute on outer most div', () => { -// const {container} = renderWithVuetify(VuetifyDemoComponent) - -// expect(container).toHaveAttribute('data-app', 'true') -// }) - -// test('renders a Vuetify-powered component', async () => { -// const {getByText} = renderWithVuetify(VuetifyDemoComponent) - -// await fireEvent.click(getByText('open')) - -// expect(getByText('Lorem ipsum dolor sit amet.')).toMatchInlineSnapshot(` -//
-// Lorem ipsum dolor sit amet. -//
-// `) -// }) - -// test('opens a menu', async () => { -// const {getByRole, getByText, queryByText} = renderWithVuetify( -// VuetifyDemoComponent, -// ) - -// const openMenuButton = getByRole('button', {name: 'open menu'}) - -// // Menu item is not rendered initially -// expect(queryByText('menu item')).not.toBeInTheDocument() - -// await fireEvent.click(openMenuButton) - -// const menuItem = getByText('menu item') -// expect(menuItem).toBeInTheDocument() - -// await fireEvent.click(openMenuButton) - -// expect(menuItem).toBeInTheDocument() -// expect(menuItem).not.toBeVisible() -// }) diff --git a/src/render.js b/src/render.js index b07b97cf..997ba4c0 100644 --- a/src/render.js +++ b/src/render.js @@ -45,7 +45,7 @@ Check out the test examples on GitHub for further details.`) : console.log(prettyDOM(el, maxLength, options)), unmount: () => wrapper.unmount(), html: () => wrapper.html(), - emitted: () => wrapper.emitted(), + emitted: name => wrapper.emitted(name), rerender: props => wrapper.setProps(props), ...getQueriesForElement(baseElement), } @@ -60,10 +60,7 @@ function cleanup() { } function cleanupAtWrapper(wrapper) { - if ( - wrapper.element.parentNode && - wrapper.element.parentNode.parentNode === document.body - ) { + if (wrapper.element?.parentNode?.parentNode === document.body) { document.body.removeChild(wrapper.element.parentNode) } diff --git a/types/index.d.ts b/types/index.d.ts index 02c93699..d4edc3bf 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,10 +1,13 @@ // Minimum TypeScript Version: 4.0 /* eslint-disable @typescript-eslint/no-explicit-any */ +import {VNodeChild} from 'vue' import {MountingOptions} from '@vue/test-utils' import {queries, EventType, BoundFunctions} from '@testing-library/dom' // eslint-disable-next-line import/no-extraneous-dependencies import {OptionsReceived as PrettyFormatOptions} from 'pretty-format' +import {ComponentProps, ComponentSlots} from 'vue-component-type-helpers' +import {RemoveIndexSignature} from 'type-fest' // NOTE: fireEvent is overridden below export * from '@testing-library/dom' @@ -24,6 +27,7 @@ export interface RenderResult extends BoundFunctions { unmount(): void html(): string emitted(): Record + emitted(name?: string): T[] rerender(props: object): Promise } @@ -43,11 +47,26 @@ interface VueTestingLibraryRenderOptions { container?: Element baseElement?: Element } -export type RenderOptions = VueTestingLibraryRenderOptions & VueTestUtilsRenderOptions -export function render( - TestComponent: any, // this makes me sad :sob: - options?: RenderOptions, +type AllowNonFunctionSlots = { + [K in keyof Slots]: Slots[K] | VNodeChild +} +type ExtractSlots = AllowNonFunctionSlots< + Partial>> +> + +export interface RenderOptions + extends Omit< + VueTestingLibraryRenderOptions & VueTestUtilsRenderOptions, + 'props' | 'slots' + > { + props?: ComponentProps + slots?: ExtractSlots +} + +export function render( + TestComponent: C, + options?: RenderOptions, ): RenderResult export type AsyncFireObject = { diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 8ec7e117..02e9e187 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -68,7 +68,7 @@ export async function testWaitFor() { export function testOptions() { render(SomeComponent, { attrs: {a: 1}, - props: {c: 1}, // ideally it would fail because `c` is not an existing prop… + props: {foo: 1}, data: () => ({b: 2}), slots: { default: '
', @@ -86,13 +86,14 @@ export function testOptions() { export function testEmitted() { const {emitted} = render(SomeComponent) expectType(emitted().foo) + expectType(emitted('foo')) } /* eslint testing-library/prefer-explicit-assert: "off", testing-library/no-wait-for-empty-callback: "off", - testing-library/no-debug: "off", + testing-library/no-debugging-utils: "off", testing-library/prefer-screen-queries: "off", @typescript-eslint/unbound-method: "off", @typescript-eslint/no-invalid-void-type: "off"