Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/__tests__/fire-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,23 @@ test.each(['input', 'change'])(

expect(console.warn).toHaveBeenCalledTimes(1)
expect(console.warn).toHaveBeenCalledWith(
`Using fireEvent.${event}() may lead to unexpected results. Please use fireEvent.update() instead.`,
`Using "fireEvent.${event}" may lead to unexpected results. Please use fireEvent.update() instead.`,
)
},
)

test('does not warn when disabled via env var', async () => {
process.env.VTL_SKIP_WARN_EVENT_UPDATE = 'true'

const {getByTestId} = render({
template: `<input type="text" data-testid="test-update" />`,
})

await fireEvent.input(getByTestId('test-update'), 'hello')

expect(console.warn).not.toHaveBeenCalled()
})

test('fireEvent.update does not trigger warning messages', async () => {
const {getByTestId} = render({
template: `<input type="text" data-testid="test-update" />`,
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/vue-apollo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import '@testing-library/jest-dom'
import fetch from 'isomorphic-unfetch'
import {render, fireEvent, screen} from '..'
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,
Expand Down
87 changes: 87 additions & 0 deletions src/fire-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* 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
// version of fireEvent. The reason is because we need to wait another
// event loop tick to allow Vue to flush and update the DOM
// More info: https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue

async function fireEvent(...args) {
dtlFireEvent(...args)
await waitFor(() => {})
}

Object.keys(dtlFireEvent).forEach(key => {
fireEvent[key] = async (...args) => {
warnOnChangeOrInputEventCalledDirectly(args[1], key)

dtlFireEvent[key](...args)
await waitFor(() => {})
}
})

fireEvent.touch = async elem => {
await fireEvent.focus(elem)
await fireEvent.blur(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/master/src/__tests__/form.js fireEvent.update = (elem, value) => {
fireEvent.update = (elem, value) => {
const tagName = elem.tagName
const type = elem.type

switch (tagName) {
case 'OPTION': {
elem.selected = true

const parentSelectElement =
elem.parentElement.tagName === 'OPTGROUP'
? elem.parentElement.parentElement
: elem.parentElement

return fireEvent.change(parentSelectElement)
}

case 'INPUT': {
if (['checkbox', 'radio'].includes(type)) {
elem.checked = true
return fireEvent.change(elem)
} else if (type === 'file') {
return fireEvent.change(elem)
} else {
elem.value = value
return fireEvent.input(elem)
}
}

case 'TEXTAREA': {
elem.value = value
return fireEvent.input(elem)
}

case 'SELECT': {
elem.value = value
return fireEvent.change(elem)
}

default:
// do nothing
}

return null
}

function warnOnChangeOrInputEventCalledDirectly(eventValue, eventKey) {
if (process.env.VTL_SKIP_WARN_EVENT_UPDATE) return

if (eventValue && (eventKey === 'change' || eventKey === 'input')) {
console.warn(
`Using "fireEvent.${eventKey}" may lead to unexpected results. Please use fireEvent.update() instead.`,
)
}
}

export {fireEvent}
177 changes: 5 additions & 172 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,176 +1,8 @@
/* eslint-disable testing-library/no-wait-for-empty-callback */
import {mount} from '@vue/test-utils'

import {
getQueriesForElement,
prettyDOM,
waitFor,
fireEvent as dtlFireEvent,
} from '@testing-library/dom'

const mountedWrappers = new Set()

function render(
Component,
{
store = null,
routes = null,
container: customContainer,
baseElement: customBaseElement,
...mountOptions
} = {},
) {
const div = document.createElement('div')
const baseElement = customBaseElement || customContainer || document.body
const container = customContainer || baseElement.appendChild(div)

const plugins = mountOptions.global?.plugins || []

if (store) {
const {createStore} = require('vuex')
plugins.push(createStore(store))
}

if (routes) {
const requiredRouter = require('vue-router')
const {createRouter, createWebHistory} =
requiredRouter.default || requiredRouter

const routerPlugin = createRouter({history: createWebHistory(), routes})
plugins.push(routerPlugin)
}

const wrapper = mount(Component, {
...mountOptions,
attachTo: container,
global: {...mountOptions.global, plugins},
})

// this removes the additional "data-v-app" div node from VTU:
// https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts#L196-L213
unwrapNode(wrapper.parentElement)

mountedWrappers.add(wrapper)

return {
container,
baseElement,
debug: (el = baseElement, maxLength, options) =>
Array.isArray(el)
? el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
: console.log(prettyDOM(el, maxLength, options)),
unmount: () => wrapper.unmount(),
html: () => wrapper.html(),
emitted: () => wrapper.emitted(),
rerender: props => wrapper.setProps(props),
...getQueriesForElement(baseElement),
}
}

function unwrapNode(node) {
node.replaceWith(...node.childNodes)
}

function cleanup() {
mountedWrappers.forEach(cleanupAtWrapper)
}

function cleanupAtWrapper(wrapper) {
if (
wrapper.element.parentNode &&
wrapper.element.parentNode.parentNode === document.body
) {
document.body.removeChild(wrapper.element.parentNode)
}

wrapper.unmount()
mountedWrappers.delete(wrapper)
}

// Vue Testing Library's version of fireEvent will call DOM Testing Library's
// version of fireEvent plus wait for one tick of the event loop to allow Vue
// to asynchronously handle the event.
// More info: https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue
async function fireEvent(...args) {
dtlFireEvent(...args)
await waitFor(() => {})
}

function suggestUpdateIfNecessary(eventValue, eventKey) {
const changeOrInputEventCalledDirectly =
eventValue && (eventKey === 'change' || eventKey === 'input')

if (changeOrInputEventCalledDirectly) {
console.warn(
`Using fireEvent.${eventKey}() may lead to unexpected results. Please use fireEvent.update() instead.`,
)
}
}

Object.keys(dtlFireEvent).forEach(key => {
fireEvent[key] = async (...args) => {
suggestUpdateIfNecessary(args[1], key)
dtlFireEvent[key](...args)
await waitFor(() => {})
}
})

fireEvent.touch = async elem => {
await fireEvent.focus(elem)
await fireEvent.blur(elem)
}

// 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/master/src/__tests__/form.js
fireEvent.update = (elem, value) => {
const tagName = elem.tagName
const type = elem.type

switch (tagName) {
case 'OPTION': {
elem.selected = true

const parentSelectElement =
elem.parentElement.tagName === 'OPTGROUP'
? elem.parentElement.parentElement
: elem.parentElement

return fireEvent.change(parentSelectElement)
}

case 'INPUT': {
if (['checkbox', 'radio'].includes(type)) {
elem.checked = true
return fireEvent.change(elem)
} else if (type === 'file') {
return fireEvent.change(elem)
} else {
elem.value = value
return fireEvent.input(elem)
}
}

case 'TEXTAREA': {
elem.value = value
return fireEvent.input(elem)
}

case 'SELECT': {
elem.value = value
return fireEvent.change(elem)
}

default:
// do nothing
}

return null
}
import {cleanup} from './render'

// If we're running in a test runner that supports afterEach then we'll
// automatically run cleanup after each test. This ensures that tests run in
// isolation from each other.
// automatically run cleanup after each test.
// This ensures that tests run in isolation from each other.
// If you don't like this, set the VTL_SKIP_AUTO_CLEANUP variable to 'true'.
if (typeof afterEach === 'function' && !process.env.VTL_SKIP_AUTO_CLEANUP) {
afterEach(() => {
Expand All @@ -179,4 +11,5 @@ if (typeof afterEach === 'function' && !process.env.VTL_SKIP_AUTO_CLEANUP) {
}

export * from '@testing-library/dom'
export {cleanup, render, fireEvent}
export {cleanup, render} from './render'
export {fireEvent} from './fire-event'
85 changes: 85 additions & 0 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable testing-library/no-wait-for-empty-callback */
import {mount} from '@vue/test-utils'

import {getQueriesForElement, prettyDOM} from '@testing-library/dom'

const mountedWrappers = new Set()

function render(
Component,
{
store = null,
routes = null,
container: customContainer,
baseElement: customBaseElement,
...mountOptions
} = {},
) {
const div = document.createElement('div')
const baseElement = customBaseElement || customContainer || document.body
const container = customContainer || baseElement.appendChild(div)

const plugins = mountOptions.global?.plugins || []

if (store) {
const {createStore} = require('vuex')
plugins.push(createStore(store))
}

if (routes) {
const requiredRouter = require('vue-router')
const {createRouter, createWebHistory} =
requiredRouter.default || requiredRouter

const routerPlugin = createRouter({history: createWebHistory(), routes})
plugins.push(routerPlugin)
}

const wrapper = mount(Component, {
...mountOptions,
attachTo: container,
global: {...mountOptions.global, plugins},
})

// this removes the additional "data-v-app" div node from VTU:
// https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts#L196-L213
unwrapNode(wrapper.parentElement)

mountedWrappers.add(wrapper)

return {
container,
baseElement,
debug: (el = baseElement, maxLength, options) =>
Array.isArray(el)
? el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
: console.log(prettyDOM(el, maxLength, options)),
unmount: () => wrapper.unmount(),
html: () => wrapper.html(),
emitted: () => wrapper.emitted(),
rerender: props => wrapper.setProps(props),
...getQueriesForElement(baseElement),
}
}

function unwrapNode(node) {
node.replaceWith(...node.childNodes)
}

function cleanup() {
mountedWrappers.forEach(cleanupAtWrapper)
}

function cleanupAtWrapper(wrapper) {
if (
wrapper.element.parentNode &&
wrapper.element.parentNode.parentNode === document.body
) {
document.body.removeChild(wrapper.element.parentNode)
}

wrapper.unmount()
mountedWrappers.delete(wrapper)
}

export {render, cleanup}