test: test for DOM listener memory leaks (#1654)
* test: test for DOM listener memory leaks * fix whitespace change, unintended
This commit is contained in:
parent
95ef639b21
commit
cbbf5abd7a
|
@ -13,7 +13,7 @@
|
||||||
import { throttleTimer } from '../../_utils/throttleTimer'
|
import { throttleTimer } from '../../_utils/throttleTimer'
|
||||||
import { on } from '../../_utils/eventBus'
|
import { on } from '../../_utils/eventBus'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { getScrollContainer } from '../../_utils/scrollContainer'
|
import { addScrollListener, getScrollContainer, removeScrollListener } from '../../_utils/scrollContainer'
|
||||||
import { get } from '../../_utils/lodash-lite'
|
import { get } from '../../_utils/lodash-lite'
|
||||||
import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
|
import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
this._element.remove()
|
this._element.remove()
|
||||||
}
|
}
|
||||||
unregisterResizeListener(this.onResize)
|
unregisterResizeListener(this.onResize)
|
||||||
document.removeEventListener('scroll', this.onResize)
|
removeScrollListener(this.onResize)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
observe,
|
observe,
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
registerResizeListener(this.onResize)
|
registerResizeListener(this.onResize)
|
||||||
document.addEventListener('scroll', this.onResize)
|
addScrollListener(this.onResize)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,25 +1,49 @@
|
||||||
import {
|
import {
|
||||||
|
accountProfileName,
|
||||||
|
closeDialogButton,
|
||||||
composeInput,
|
composeInput,
|
||||||
getNthAutosuggestionResult,
|
getNthAutosuggestionResult,
|
||||||
|
getNthStatusMediaButton, getNthStatusSensitiveMediaButton,
|
||||||
|
getNthStatus,
|
||||||
getNumSyntheticListeners,
|
getNumSyntheticListeners,
|
||||||
getUrl,
|
getUrl,
|
||||||
homeNavButton,
|
homeNavButton, modalDialog, notificationsNavButton,
|
||||||
scrollToStatus,
|
scrollToStatus,
|
||||||
scrollToTop,
|
scrollToTop,
|
||||||
settingsNavButton, sleep
|
settingsNavButton, sleep
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { installDomListenerListener, getNumDomListeners } from '../spyDomListeners'
|
||||||
|
import { homeTimeline } from '../fixtures'
|
||||||
|
import { Selector as $ } from 'testcafe'
|
||||||
|
|
||||||
fixture`038-memory-leaks.js`
|
fixture`038-memory-leaks.js`
|
||||||
.page`http://localhost:4002`
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
async function goToStartPoint (t) {
|
async function runMemoryLeakTest (t, firstStep, secondStep) {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await installDomListenerListener()
|
||||||
|
await firstStep()
|
||||||
|
await sleep(1000)
|
||||||
|
const numSyntheticListeners = await getNumSyntheticListeners()
|
||||||
|
const numDomListeners = await getNumDomListeners()
|
||||||
|
await t
|
||||||
|
.expect(numSyntheticListeners).typeOf('number')
|
||||||
|
.expect(numDomListeners).typeOf('number')
|
||||||
|
await secondStep()
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.expect(getNumSyntheticListeners()).eql(numSyntheticListeners)
|
||||||
|
.expect(getNumDomListeners()).eql(numDomListeners)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goToSettings (t) {
|
||||||
await t
|
await t
|
||||||
.click(settingsNavButton)
|
.click(settingsNavButton)
|
||||||
.expect(getUrl()).contains('/settings')
|
.expect(getUrl()).contains('/settings')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function interactAndGoToEndPoint (t) {
|
async function scrollUpAndDownAndDoAutosuggest (t) {
|
||||||
await t
|
await t
|
||||||
.click(homeNavButton)
|
.click(homeNavButton)
|
||||||
.expect(getUrl()).eql('http://localhost:4002/')
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
|
@ -28,19 +52,57 @@ async function interactAndGoToEndPoint (t) {
|
||||||
await t
|
await t
|
||||||
.typeText(composeInput, 'hey @qu')
|
.typeText(composeInput, 'hey @qu')
|
||||||
.expect(getNthAutosuggestionResult(1).find('.sr-only').innerText).contains('@quux')
|
.expect(getNthAutosuggestionResult(1).find('.sr-only').innerText).contains('@quux')
|
||||||
.click(settingsNavButton)
|
await goToSettings(t)
|
||||||
.expect(getUrl()).contains('/settings')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Does not leak synthetic listeners', async t => {
|
async function openAndCloseMediaModal (t) {
|
||||||
await loginAsFoobar(t)
|
|
||||||
await goToStartPoint(t)
|
|
||||||
await sleep(1000)
|
|
||||||
const numSyntheticListeners = await getNumSyntheticListeners()
|
|
||||||
await t
|
await t
|
||||||
.expect(numSyntheticListeners).typeOf('number')
|
.click(homeNavButton)
|
||||||
await interactAndGoToEndPoint(t)
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
await sleep(1000)
|
const idx = homeTimeline.findIndex(_ => _.spoiler === 'kitten CW')
|
||||||
|
await scrollToStatus(t, idx + 1)
|
||||||
await t
|
await t
|
||||||
.expect(getNumSyntheticListeners()).eql(numSyntheticListeners)
|
.click(getNthStatusSensitiveMediaButton(idx + 1))
|
||||||
|
.click(getNthStatusMediaButton(idx + 1))
|
||||||
|
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
||||||
|
.click(closeDialogButton)
|
||||||
|
.expect(modalDialog.exists).notOk()
|
||||||
|
await goToSettings(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openAProfileAndNotifications (t) {
|
||||||
|
await t
|
||||||
|
.click(homeNavButton)
|
||||||
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
|
.hover(getNthStatus(1))
|
||||||
|
.click($('.status-author-name').withText(('quux')))
|
||||||
|
.expect(getUrl()).contains('/accounts/3')
|
||||||
|
.expect(accountProfileName.innerText).contains('quux')
|
||||||
|
.click(notificationsNavButton)
|
||||||
|
.hover(getNthStatus(1))
|
||||||
|
await goToSettings(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Does not leak listeners in timeline or autosuggest', async t => {
|
||||||
|
await runMemoryLeakTest(
|
||||||
|
t,
|
||||||
|
() => goToSettings(t),
|
||||||
|
() => scrollUpAndDownAndDoAutosuggest(t)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Does not leak listeners in modal', async t => {
|
||||||
|
await runMemoryLeakTest(
|
||||||
|
t,
|
||||||
|
() => goToSettings(t),
|
||||||
|
() => openAndCloseMediaModal(t)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Does not leak listeners in account profile or notifications page', async t => {
|
||||||
|
await runMemoryLeakTest(
|
||||||
|
t,
|
||||||
|
() => goToSettings(t),
|
||||||
|
() => openAProfileAndNotifications(t)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
87
tests/spyDomListeners.js
Normal file
87
tests/spyDomListeners.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { ClientFunction as exec } from 'testcafe'
|
||||||
|
|
||||||
|
export const installDomListenerListener = exec(() => {
|
||||||
|
function eql (a, b) {
|
||||||
|
const aType = typeof a
|
||||||
|
const bType = typeof b
|
||||||
|
switch (aType) {
|
||||||
|
case 'boolean':
|
||||||
|
return bType === 'boolean' && b === a
|
||||||
|
case 'undefined':
|
||||||
|
return bType === 'undefined'
|
||||||
|
case 'object':
|
||||||
|
if (a === null) {
|
||||||
|
return b === null
|
||||||
|
}
|
||||||
|
if (b === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
if (a[key] !== b[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function spyAddListener (proto) {
|
||||||
|
const addEventListener = proto.addEventListener
|
||||||
|
proto.addEventListener = function (type, listener, options) {
|
||||||
|
if (!this.__listeners) {
|
||||||
|
this.__listeners = {}
|
||||||
|
}
|
||||||
|
if (!this.__listeners[type]) {
|
||||||
|
this.__listeners[type] = []
|
||||||
|
}
|
||||||
|
this.__listeners[type].push({ listener, options })
|
||||||
|
return addEventListener.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spyRemoveListener (proto) {
|
||||||
|
const removeEventListener = proto.removeEventListener
|
||||||
|
proto.removeEventListener = function (type, listener, options) {
|
||||||
|
if (this.__listeners && this.__listeners[type]) {
|
||||||
|
const arr = this.__listeners[type]
|
||||||
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
|
const { listener: otherListener, options: otherOptions } = arr[i]
|
||||||
|
if (listener === otherListener && eql(options, otherOptions)) {
|
||||||
|
arr.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removeEventListener.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spy (proto) {
|
||||||
|
spyAddListener(proto)
|
||||||
|
spyRemoveListener(proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
spy(Element.prototype)
|
||||||
|
spy(document)
|
||||||
|
spy(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getNumDomListeners = exec(() => {
|
||||||
|
function getNumListeners (obj) {
|
||||||
|
let sum = 0
|
||||||
|
if (obj.__listeners) {
|
||||||
|
for (const key of Object.keys(obj.__listeners)) {
|
||||||
|
sum += obj.__listeners[key].length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...document.querySelectorAll('*')]
|
||||||
|
.concat([window, document])
|
||||||
|
.map(getNumListeners)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
})
|
Loading…
Reference in a new issue