describe(`runtime-dom: events patching`, () => {
it('should assign event handler', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn = jest.fn()
patchProp(el, 'onClick', null, fn)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn).toHaveBeenCalledTimes(3)
})
it('should update event handler', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const prevFn = jest.fn()
const nextFn = jest.fn()
patchProp(el, 'onClick', null, prevFn)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
patchProp(el, 'onClick', prevFn, nextFn)
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(prevFn).toHaveBeenCalledTimes(1)
expect(nextFn).toHaveBeenCalledTimes(2)
it('should support multiple event handlers', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn1 = jest.fn()
const fn2 = jest.fn()
patchProp(el, 'onClick', null, [fn1, fn2])
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
it('should unassign event handler', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn = jest.fn()
patchProp(el, 'onClick', null, fn)
patchProp(el, 'onClick', fn, null)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn).not.toHaveBeenCalled()
})
it('should support event option modifiers', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn = jest.fn()
patchProp(el, 'onClickOnceCapture', null, fn)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
})
it('should unassign event handler with options', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn = jest.fn()
patchProp(el, 'onClickCapture', null, fn)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
patchProp(el, 'onClickCapture', fn, null)
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
})
it('should support native onclick', async () => {
const el = document.createElement('div')
- const event = new Event('click')
// string should be set as attribute
const fn = ((window as any).__globalSpy = jest.fn())
patchProp(el, 'onclick', null, '__globalSpy(1)')
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
delete (window as any).__globalSpy
expect(fn).toHaveBeenCalledWith(1)
const fn2 = jest.fn()
patchProp(el, 'onclick', '__globalSpy(1)', fn2)
+ const event = new Event('click')
el.dispatchEvent(event)
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
it('should support stopImmediatePropagation on multiple listeners', async () => {
const el = document.createElement('div')
- const event = new Event('click')
const fn1 = jest.fn((e: Event) => {
e.stopImmediatePropagation()
})
const fn2 = jest.fn()
patchProp(el, 'onClick', null, [fn1, fn2])
- el.dispatchEvent(event)
+ el.dispatchEvent(new Event('click'))
await timeout()
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(0)
const el1 = document.createElement('div')
const el2 = document.createElement('div')
- const event = new Event('click')
+ // const event = new Event('click')
const prevFn = jest.fn()
const nextFn = jest.fn()
patchProp(el1, 'onClick', null, prevFn)
patchProp(el2, 'onClick', null, prevFn)
- el1.dispatchEvent(event)
- el2.dispatchEvent(event)
+ el1.dispatchEvent(new Event('click'))
+ el2.dispatchEvent(new Event('click'))
await timeout()
expect(prevFn).toHaveBeenCalledTimes(2)
expect(nextFn).toHaveBeenCalledTimes(0)
patchProp(el1, 'onClick', prevFn, nextFn)
patchProp(el2, 'onClick', prevFn, nextFn)
- el1.dispatchEvent(event)
- el2.dispatchEvent(event)
+ el1.dispatchEvent(new Event('click'))
+ el2.dispatchEvent(new Event('click'))
await timeout()
expect(prevFn).toHaveBeenCalledTimes(2)
expect(nextFn).toHaveBeenCalledTimes(2)
- el1.dispatchEvent(event)
- el2.dispatchEvent(event)
+ el1.dispatchEvent(new Event('click'))
+ el2.dispatchEvent(new Event('click'))
await timeout()
expect(prevFn).toHaveBeenCalledTimes(2)
expect(nextFn).toHaveBeenCalledTimes(4)
})
+ // vuejs/vue#6566
+ it('should not fire handler attached by the event itself', async () => {
+ const el = document.createElement('div')
+ const child = document.createElement('div')
+ el.appendChild(child)
+ document.body.appendChild(el)
+ const childFn = jest.fn()
+ const parentFn = jest.fn()
+
+ patchProp(child, 'onClick', null, () => {
+ childFn()
+ patchProp(el, 'onClick', null, parentFn)
+ })
+ child.dispatchEvent(new Event('click', { bubbles: true }))
+
+ await timeout()
+ expect(childFn).toHaveBeenCalled()
+ expect(parentFn).not.toHaveBeenCalled()
+ })
+
// #2841
test('should patch event correctly in web-components', async () => {
class TestElement extends HTMLElement {
type EventValue = Function | Function[]
-// Async edge case fix requires storing an event listener's attach timestamp.
-const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
- let _getNow = Date.now
- let skipTimestampCheck = false
- if (typeof window !== 'undefined') {
- // Determine what event timestamp the browser is using. Annoyingly, the
- // timestamp can either be hi-res (relative to page load) or low-res
- // (relative to UNIX epoch), so in order to compare time we have to use the
- // same timestamp type when saving the flush timestamp.
- if (Date.now() > document.createEvent('Event').timeStamp) {
- // if the low-res timestamp which is bigger than the event timestamp
- // (which is evaluated AFTER) it means the event is using a hi-res timestamp,
- // and we need to use the hi-res version for event listeners as well.
- _getNow = performance.now.bind(performance)
- }
- // #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
- // and does not fire microtasks in between event propagation, so safe to exclude.
- const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i)
- skipTimestampCheck = !!(ffMatch && Number(ffMatch[1]) <= 53)
- }
- return [_getNow, skipTimestampCheck]
-})()
-
-// To avoid the overhead of repeatedly calling performance.now(), we cache
-// and use the same timestamp for all event listeners attached in the same tick.
-let cachedNow: number = 0
-const p = /*#__PURE__*/ Promise.resolve()
-const reset = () => {
- cachedNow = 0
-}
-const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
-
export function addEventListener(
el: Element,
event: string,
return [event, options]
}
+// To avoid the overhead of repeatedly calling Date.now(), we cache
+// and use the same timestamp for all event listeners attached in the same tick.
+let cachedNow: number = 0
+const p = /*#__PURE__*/ Promise.resolve()
+const getNow = () =>
+ cachedNow || (p.then(() => (cachedNow = 0)), (cachedNow = Date.now()))
+
function createInvoker(
initialValue: EventValue,
instance: ComponentInternalInstance | null
) {
- const invoker: Invoker = (e: Event) => {
- // async edge case #6566: inner click event triggers patch, event handler
+ const invoker: Invoker = (e: Event & { _vts?: number }) => {
+ // async edge case vuejs/vue#6566
+ // inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
- // the solution is simple: we save the timestamp when a handler is attached,
- // and the handler would only fire if the event passed to it was fired
+ // this no longer happens for templates in Vue 3, but could still be
+ // theoretically possible for hand-written render functions.
+ // the solution: we save the timestamp when a handler is attached,
+ // and also attach the timestamp to any event that was handled by vue
+ // for the first time (to avoid inconsistent event timestamp implementations
+ // or events fired from iframes, e.g. #2513)
+ // The handler would only fire if the event passed to it was fired
// AFTER it was attached.
- const timeStamp = e.timeStamp || _getNow()
-
- if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
- callWithAsyncErrorHandling(
- patchStopImmediatePropagation(e, invoker.value),
- instance,
- ErrorCodes.NATIVE_EVENT_HANDLER,
- [e]
- )
+ if (!e._vts) {
+ e._vts = Date.now()
+ } else if (e._vts <= invoker.attached) {
+ return
}
+ callWithAsyncErrorHandling(
+ patchStopImmediatePropagation(e, invoker.value),
+ instance,
+ ErrorCodes.NATIVE_EVENT_HANDLER,
+ [e]
+ )
}
invoker.value = initialValue
invoker.attached = getNow()