node.
-const renderWithVuetify = (component, options, callback) => {
- const root = document.createElement('div')
- root.setAttribute('data-app', 'true')
+// // 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,
- )
-}
+// 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)
+// test('should set [data-app] attribute on outer most div', () => {
+// const {container} = renderWithVuetify(VuetifyDemoComponent)
- expect(container).toHaveAttribute('data-app', 'true')
-})
+// expect(container).toHaveAttribute('data-app', 'true')
+// })
-test('renders a Vuetify-powered component', async () => {
- const {getByText} = renderWithVuetify(VuetifyDemoComponent)
+// test('renders a Vuetify-powered component', async () => {
+// const {getByText} = renderWithVuetify(VuetifyDemoComponent)
- await fireEvent.click(getByText('open'))
+// await fireEvent.click(getByText('open'))
- expect(getByText('Lorem ipsum dolor sit amet.')).toMatchInlineSnapshot(`
-
- Lorem ipsum dolor sit amet.
-
- `)
-})
+// expect(getByText('Lorem ipsum dolor sit amet.')).toMatchInlineSnapshot(`
+//
+// Lorem ipsum dolor sit amet.
+//
+// `)
+// })
-test('opens a menu', async () => {
- const {getByRole, getByText, queryByText} = renderWithVuetify(
- VuetifyDemoComponent,
- )
+// test('opens a menu', async () => {
+// const {getByRole, getByText, queryByText} = renderWithVuetify(
+// VuetifyDemoComponent,
+// )
- const openMenuButton = getByRole('button', {name: 'open menu'})
+// const openMenuButton = getByRole('button', {name: 'open menu'})
- // Menu item is not rendered initially
- expect(queryByText('menu item')).not.toBeInTheDocument()
+// // Menu item is not rendered initially
+// expect(queryByText('menu item')).not.toBeInTheDocument()
- await fireEvent.click(openMenuButton)
+// await fireEvent.click(openMenuButton)
- const menuItem = getByText('menu item')
- expect(menuItem).toBeInTheDocument()
+// const menuItem = getByText('menu item')
+// expect(menuItem).toBeInTheDocument()
- await fireEvent.click(openMenuButton)
+// await fireEvent.click(openMenuButton)
- expect(menuItem).toBeInTheDocument()
- expect(menuItem).not.toBeVisible()
-})
+// expect(menuItem).toBeInTheDocument()
+// expect(menuItem).not.toBeVisible()
+// })
diff --git a/src/__tests__/vuex.js b/src/__tests__/vuex.js
index c41008bf..d0faa53a 100644
--- a/src/__tests__/vuex.js
+++ b/src/__tests__/vuex.js
@@ -1,11 +1,30 @@
import '@testing-library/jest-dom'
-import {render, fireEvent} from '@testing-library/vue'
-import Vue from 'vue'
-import Vuex from 'vuex'
-
+import {createStore} from 'vuex'
+import {render, fireEvent} from '..'
import VuexTest from './components/Store/VuexTest'
import {store} from './components/Store/store'
+test('basic test with Vuex store', async () => {
+ const storeInstance = createStore(store)
+
+ const {getByTestId, getByText} = render(VuexTest, {
+ global: {
+ plugins: [storeInstance],
+ },
+ })
+
+ expect(getByTestId('count-value')).toHaveTextContent('0')
+
+ await fireEvent.click(getByText('+'))
+ expect(getByTestId('count-value')).toHaveTextContent('1')
+
+ await fireEvent.click(getByText('+'))
+ expect(getByTestId('count-value')).toHaveTextContent('2')
+
+ await fireEvent.click(getByText('-'))
+ expect(getByTestId('count-value')).toHaveTextContent('1')
+})
+
// A common testing pattern is to create a custom renderer for a specific test
// file. This way, common operations such as registering a Vuex store can be
// abstracted out while avoiding sharing mutable state.
@@ -13,14 +32,20 @@ import {store} from './components/Store/store'
// Tests should be completely isolated from one another.
// Read this for additional context: https://kentcdodds.com/blog/test-isolation-with-react
function renderVuexTestComponent(customStore) {
- // Render the component and merge the original store and the custom one
- // provided as a parameter. This way, we can alter some behaviors of the
- // initial implementation.
- return render(VuexTest, {store: {...store, ...customStore}})
+ // Create a custom store with the original one and the one coming as a
+ // parameter. This way we can alter some of its values.
+ const mergedStoreInstance = createStore({...store, ...customStore})
+
+ return render(VuexTest, {
+ global: {
+ plugins: [mergedStoreInstance],
+ },
+ })
}
test('can render with vuex with defaults', async () => {
const {getByTestId, getByText} = renderVuexTestComponent()
+
await fireEvent.click(getByText('+'))
expect(getByTestId('count-value')).toHaveTextContent('1')
@@ -28,8 +53,9 @@ test('can render with vuex with defaults', async () => {
test('can render with vuex with custom initial state', async () => {
const {getByTestId, getByText} = renderVuexTestComponent({
- state: {count: 3},
+ state: () => ({count: 3}),
})
+
await fireEvent.click(getByText('-'))
expect(getByTestId('count-value')).toHaveTextContent('2')
@@ -38,57 +64,25 @@ test('can render with vuex with custom initial state', async () => {
test('can render with vuex with custom store', async () => {
// This is a silly store that can never be changed.
// eslint-disable-next-line no-shadow
- const store = {
- state: {count: 1000},
+ const store = createStore({
+ state: () => ({count: 1000}),
actions: {
increment: () => jest.fn(),
decrement: () => jest.fn(),
},
- }
-
- // Notice how here we are not using the helper method, because there's no
- // need to do that.
- const {getByTestId, getByText} = render(VuexTest, {store})
-
- await fireEvent.click(getByText('+'))
- expect(getByTestId('count-value')).toHaveTextContent('1000')
-
- await fireEvent.click(getByText('-'))
- expect(getByTestId('count-value')).toHaveTextContent('1000')
-})
-
-test('can render with an instantiated Vuex store', async () => {
- // Before calling new Vuex.Store in your code, you'll need to `.use` it on the
- // global (or local) Vue instance. In frameworks like Jest, the `setupFilesAfterEnv`
- // property is a good place to do this.
- // https://jestjs.io/docs/configuration#setupfilesafterenv-array
- Vue.use(Vuex)
+ })
+ // Notice how here we are not using the helper rendering method, because
+ // there's no need to do that here. We're passing a whole store.
const {getByTestId, getByText} = render(VuexTest, {
- store: new Vuex.Store({
- state: {count: 3},
- mutations: {
- increment(state) {
- state.count++
- },
- decrement(state) {
- state.count--
- },
- },
- actions: {
- increment(context) {
- context.commit('increment')
- },
- decrement(context) {
- context.commit('decrement')
- },
- },
- }),
+ global: {
+ plugins: [store],
+ },
})
await fireEvent.click(getByText('+'))
- expect(getByTestId('count-value')).toHaveTextContent('4')
+ expect(getByTestId('count-value')).toHaveTextContent('1000')
await fireEvent.click(getByText('-'))
- expect(getByTestId('count-value')).toHaveTextContent('3')
+ expect(getByTestId('count-value')).toHaveTextContent('1000')
})
diff --git a/src/__tests__/within.js b/src/__tests__/within.js
index 23780f96..53f70bab 100644
--- a/src/__tests__/within.js
+++ b/src/__tests__/within.js
@@ -1,4 +1,4 @@
-import {render, within} from '@testing-library/vue'
+import {render, within} from '..'
test('within() returns an object with all queries bound to the DOM node', () => {
const {getByTestId, getByText} = render({
diff --git a/src/fire-event.js b/src/fire-event.js
index 62497c34..eca4438c 100644
--- a/src/fire-event.js
+++ b/src/fire-event.js
@@ -1,4 +1,4 @@
-/* eslint-disable testing-library/no-wait-for-empty-callback, testing-library/await-fire-event */
+/* eslint-disable testing-library/no-wait-for-empty-callback */
import {waitFor, fireEvent as dtlFireEvent} from '@testing-library/dom'
// Vue Testing Lib's version of fireEvent will call DOM Testing Lib's
@@ -28,7 +28,7 @@ fireEvent.touch = async elem => {
// fireEvent.update is a small utility to provide a better experience when
// working with v-model.
// Related upstream issue: https://github.com/vuejs/vue-test-utils/issues/345#issuecomment-380588199
-// Examples: https://github.com/testing-library/vue-testing-library/blob/main/src/__tests__/form.js
+// See some examples in __tests__/form.js
fireEvent.update = (elem, value) => {
const tagName = elem.tagName
const type = elem.type
@@ -53,18 +53,12 @@ fireEvent.update = (elem, value) => {
return fireEvent.change(elem)
} else {
elem.value = value
- if (elem._vModifiers?.lazy) {
- return fireEvent.change(elem)
- }
return fireEvent.input(elem)
}
}
case 'TEXTAREA': {
elem.value = value
- if (elem._vModifiers?.lazy) {
- return fireEvent.change(elem)
- }
return fireEvent.input(elem)
}
diff --git a/src/render.js b/src/render.js
index ec2b6db2..b07b97cf 100644
--- a/src/render.js
+++ b/src/render.js
@@ -1,4 +1,5 @@
-import {createLocalVue, mount} from '@vue/test-utils'
+/* eslint-disable testing-library/no-wait-for-empty-callback */
+import {mount} from '@vue/test-utils'
import {getQueriesForElement, prettyDOM} from '@testing-library/dom'
@@ -13,72 +14,47 @@ function render(
baseElement: customBaseElement,
...mountOptions
} = {},
- configurationCb,
) {
const div = document.createElement('div')
const baseElement = customBaseElement || customContainer || document.body
const container = customContainer || baseElement.appendChild(div)
- const attachTo = document.createElement('div')
- container.appendChild(attachTo)
-
- const localVue = createLocalVue()
- let vuexStore = null
- let router = null
- let callbackOptions = {}
-
- if (store) {
- const Vuex = require('vuex')
- localVue.use(Vuex)
-
- vuexStore = store instanceof Vuex.Store ? store : new Vuex.Store(store)
- }
-
- if (routes) {
- const requiredRouter = require('vue-router')
- const VueRouter = requiredRouter.default || requiredRouter
- localVue.use(VueRouter)
-
- router = routes instanceof VueRouter ? routes : new VueRouter({routes})
- }
-
- if (configurationCb && typeof configurationCb === 'function') {
- callbackOptions = configurationCb(localVue, vuexStore, router)
- }
-
- if (!mountOptions.propsData && !!mountOptions.props) {
- mountOptions.propsData = mountOptions.props
- delete mountOptions.props
+ if (store || routes) {
+ console.warn(`Providing 'store' or 'routes' options is no longer available.
+You need to create a router/vuex instance and provide it through 'global.plugins'.
+Check out the test examples on GitHub for further details.`)
}
const wrapper = mount(Component, {
- attachTo,
- localVue,
- router,
- store: vuexStore,
...mountOptions,
- ...callbackOptions,
+ attachTo: container,
})
+ // this removes the additional wrapping div node from VTU:
+ // https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts#L309
+ unwrapNode(wrapper.parentElement)
+
mountedWrappers.add(wrapper)
- container.appendChild(wrapper.element)
return {
container,
baseElement,
- debug: (el = baseElement, ...args) =>
+ debug: (el = baseElement, maxLength, options) =>
Array.isArray(el)
- ? el.forEach(e => console.log(prettyDOM(e, ...args)))
- : console.log(prettyDOM(el, ...args)),
- unmount: () => wrapper.destroy(),
- isUnmounted: () => wrapper.vm._isDestroyed,
+ ? el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
+ : console.log(prettyDOM(el, maxLength, options)),
+ unmount: () => wrapper.unmount(),
html: () => wrapper.html(),
emitted: () => wrapper.emitted(),
- updateProps: _ => wrapper.setProps(_),
+ rerender: props => wrapper.setProps(props),
...getQueriesForElement(baseElement),
}
}
+function unwrapNode(node) {
+ node.replaceWith(...node.childNodes)
+}
+
function cleanup() {
mountedWrappers.forEach(cleanupAtWrapper)
}
@@ -91,11 +67,8 @@ function cleanupAtWrapper(wrapper) {
document.body.removeChild(wrapper.element.parentNode)
}
- try {
- wrapper.destroy()
- } finally {
- mountedWrappers.delete(wrapper)
- }
+ wrapper.unmount()
+ mountedWrappers.delete(wrapper)
}
-export {cleanup, render}
+export {render, cleanup}
diff --git a/types/index.d.ts b/types/index.d.ts
index 28bc4382..b6037f31 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1,72 +1,54 @@
-// TypeScript Version: 4.0
+// Minimum TypeScript Version: 4.0
+/* eslint-disable @typescript-eslint/no-explicit-any */
-import Vue, {ComponentOptions} from 'vue'
-import {ThisTypedMountOptions, VueClass} from '@vue/test-utils'
+import {MountingOptions} from '@vue/test-utils'
+import {queries, EventType, BoundFunctions} from '@testing-library/dom'
// eslint-disable-next-line import/no-extraneous-dependencies
-import {
- queries,
- EventType,
- BoundFunctions,
- prettyFormat,
-} from '@testing-library/dom'
+import {OptionsReceived as PrettyFormatOptions} from 'pretty-format'
// NOTE: fireEvent is overridden below
export * from '@testing-library/dom'
export function cleanup(): void
+type Debug = (
+ baseElement?: Array
| DocumentFragment | Element,
+ maxLength?: number,
+ options?: PrettyFormatOptions,
+) => void
+
export interface RenderResult extends BoundFunctions {
container: Element
baseElement: Element
- debug: (
- baseElement?:
- | Element
- | DocumentFragment
- | Array,
- maxLength?: number,
- options?: prettyFormat.OptionsReceived,
- ) => void
+ debug: Debug
unmount(): void
- isUnmounted(): boolean
html(): string
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- emitted(): {[name: string]: any[][]}
- updateProps(props: object): Promise
+ emitted(): Record
+ rerender(props: object): Promise
}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Store = any
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Routes = any
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Router = any
-
-export interface RenderOptions
- // The props and store options special-cased by Vue Testing Library and NOT passed to mount().
- extends Omit, 'store' | 'props'> {
- props?: object
- store?: S
- routes?: Routes
+type VueTestUtilsRenderOptions = Omit<
+ MountingOptions>,
+ 'attachTo' | 'propsData' | 'shallow'
+>
+interface VueTestingLibraryRenderOptions {
+ /**
+ * @deprecated Add a Vuex instance through `global.plugins` array instead.
+ */
+ store?: any
+ /**
+ * @deprecated Add a Router instance through `global.plugins` array instead.
+ */
+ routes?: any
container?: Element
baseElement?: Element
}
+export type RenderOptions = VueTestingLibraryRenderOptions &
+ VueTestUtilsRenderOptions
-export type ConfigurationCallback =
- | ((
- localVue: typeof Vue,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- store: Store,
- router: Router,
- ) => Partial>)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- | ((localVue: typeof Vue, store: Store, router: Router) => void)
-
-export function render(
- TestComponent: VueClass | ComponentOptions,
- // eslint-disable-next-line @typescript-eslint/ban-tslint-comment
- // tslint:disable-next-line no-unnecessary-generics
- options?: RenderOptions,
- configure?: ConfigurationCallback,
+export function render(
+ TestComponent: any, // this makes me sad :sob:
+ options?: RenderOptions,
): RenderResult
export type AsyncFireObject = {
diff --git a/types/index.test-d.ts b/types/index.test-d.ts
new file mode 100644
index 00000000..8ec7e117
--- /dev/null
+++ b/types/index.test-d.ts
@@ -0,0 +1,99 @@
+import {expectType} from 'tsd'
+import {defineComponent} from 'vue'
+import {render, fireEvent, screen, waitFor} from '.'
+
+declare const elem: Element
+
+const SomeComponent = defineComponent({
+ name: 'SomeComponent',
+ props: {
+ foo: {type: Number, default: 0},
+ bar: {type: String, default: '0'},
+ },
+})
+
+export async function testRender() {
+ const utils = render({template: ''})
+
+ // single queries
+ expectType(utils.getByText('foo'))
+ expectType(utils.queryByText('foo'))
+ expectType(await utils.findByText('foo'))
+
+ // multiple queries
+ expectType(utils.getAllByText('bar'))
+ expectType(utils.queryAllByText('bar'))
+ expectType(await utils.findAllByText('bar'))
+
+ // helpers
+ const {container, baseElement, unmount, debug, rerender} = utils
+
+ expectType(await rerender({a: 1}))
+
+ expectType(debug())
+ expectType(debug(container))
+ expectType(debug([elem, elem], 100, {highlight: false}))
+
+ expectType(unmount())
+
+ expectType(container)
+ expectType(baseElement)
+}
+
+export function testRenderOptions() {
+ const container = document.createElement('div')
+ const baseElement = document.createElement('div')
+ const options = {container, baseElement}
+ render({template: 'div'}, options)
+}
+
+export async function testFireEvent() {
+ const {container} = render({template: 'button'})
+ expectType(await fireEvent.click(container))
+ expectType(await fireEvent.touch(elem))
+}
+
+export async function testScreen() {
+ render({template: 'button'})
+
+ expectType(await screen.findByRole('button'))
+}
+
+export async function testWaitFor() {
+ const {container} = render({template: 'button'})
+ expectType(await fireEvent.update(container))
+ expectType(await waitFor(() => {}))
+}
+
+export function testOptions() {
+ render(SomeComponent, {
+ attrs: {a: 1},
+ props: {c: 1}, // ideally it would fail because `c` is not an existing prop…
+ data: () => ({b: 2}),
+ slots: {
+ default: '',
+ footer: '',
+ },
+ global: {
+ config: {isCustomElement: _ => true},
+ plugins: [],
+ },
+ baseElement: document.createElement('div'),
+ container: document.createElement('div'),
+ })
+}
+
+export function testEmitted() {
+ const {emitted} = render(SomeComponent)
+ 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/prefer-screen-queries: "off",
+ @typescript-eslint/unbound-method: "off",
+ @typescript-eslint/no-invalid-void-type: "off"
+*/
diff --git a/types/test.ts b/types/test.ts
deleted file mode 100644
index f749e4c7..00000000
--- a/types/test.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
-import VueRouter from 'vue-router'
-import {render, fireEvent, screen, waitFor} from '@testing-library/vue'
-
-declare const elem: Element
-
-const SomeComponent = Vue.extend({
- name: 'SomeComponent',
- props: {
- foo: {type: Number, default: 0},
- bar: {type: String, default: '0'},
- },
-})
-
-export async function testRender() {
- const {
- getByText,
- queryByText,
- findByText,
- getAllByText,
- queryAllByText,
- findAllByText,
- container,
- unmount,
- debug,
- } = render({template: ''})
-
- // single queries
- getByText('foo')
- queryByText('foo')
- await findByText('foo')
-
- // multiple queries
- getAllByText('bar')
- queryAllByText('bar')
- await findAllByText('bar')
-
- debug(container)
-
- debug(elem) // $ExpectType void
- debug([elem, elem], 100, {highlight: false}) // $ExpectType void
-
- unmount()
-}
-
-export function testRenderOptions() {
- const container = document.createElement('div')
- const options = {container}
- render({template: 'div'}, options)
-}
-
-export async function testFireEvent() {
- const {container} = render({template: 'button'})
- await fireEvent.click(container)
-}
-
-export function testDebug() {
- const {debug, getAllByTestId} = render({
- render(h) {
- return h('div', [
- h('h1', {attrs: {'data-testId': 'testid'}}, 'hello world'),
- h('h2', {attrs: {'data-testId': 'testid'}}, 'hello world'),
- ])
- },
- })
-
- debug(getAllByTestId('testid'))
-}
-
-export async function testScreen() {
- render({template: 'button'})
-
- await screen.findByRole('button')
-}
-
-export async function testWaitFor() {
- const {container} = render({template: 'button'})
- await fireEvent.click(container)
- await waitFor(() => {})
-}
-
-export function testOptions() {
- render(SomeComponent, {
- // options for new Vue()
- name: 'SomeComponent',
- methods: {
- glorb() {
- return 42
- },
- },
- // options for vue-test-utils mount()
- slots: {
- quux: 'Baz
',
- },
- mocks: {
- isThisFake() {
- return true
- },
- },
- // options for Vue Testing Library render()
- container: elem,
- baseElement: elem,
- props: {
- foo: 9,
- bar: 'x',
- },
- store: {
- state: {
- foos: [4, 5],
- bars: ['a', 'b'],
- },
- },
- routes: [
- {path: '/', name: 'home', component: SomeComponent},
- {
- path: '/about',
- name: 'about',
- component: () => Promise.resolve(SomeComponent),
- },
- ],
- })
-}
-
-export function testConfigCallback() {
- const ExamplePlugin: Vue.PluginFunction = () => {}
- render(SomeComponent, {}, localVue => {
- localVue.use(ExamplePlugin)
- })
-}
-
-export function testInstantiatedStore() {
- render(SomeComponent, {
- store: new Vuex.Store({
- state: {count: 3},
- mutations: {
- increment(state) {
- state.count++
- },
- decrement(state) {
- state.count--
- },
- },
- actions: {
- increment(context) {
- context.commit('increment')
- },
- decrement(context) {
- context.commit('decrement')
- },
- },
- }),
- })
-}
-
-export function testInstantiatedRouter() {
- render(SomeComponent, {
- routes: new VueRouter({
- routes: [{path: '/', name: 'home', component: SomeComponent}],
- }),
- })
-}
-
-/*
-eslint
- testing-library/prefer-explicit-assert: "off",
- testing-library/no-wait-for-empty-callback: "off",
- testing-library/no-debug: "off",
- testing-library/prefer-screen-queries: "off",
- @typescript-eslint/unbound-method: "off",
-*/
diff --git a/types/tsconfig.json b/types/tsconfig.json
index 4c48bf28..d822978b 100644
--- a/types/tsconfig.json
+++ b/types/tsconfig.json
@@ -3,7 +3,7 @@
{
"compilerOptions": {
"module": "commonjs",
- "lib": ["es6", "dom"],
+ "lib": ["ES2020", "DOM"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
diff --git a/types/tslint.json b/types/tslint.json
index c7b428f3..70c4494b 100644
--- a/types/tslint.json
+++ b/types/tslint.json
@@ -1,5 +1,5 @@
{
- "extends": ["dtslint/dtslint.json"],
+ "extends": "dtslint/dtslint.json",
"rules": {
"semicolon": false,
"whitespace": false