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 { on } from '../../_utils/eventBus'
|
||||
import { store } from '../../_store/store'
|
||||
import { getScrollContainer } from '../../_utils/scrollContainer'
|
||||
import { addScrollListener, getScrollContainer, removeScrollListener } from '../../_utils/scrollContainer'
|
||||
import { get } from '../../_utils/lodash-lite'
|
||||
import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
|||
this._element.remove()
|
||||
}
|
||||
unregisterResizeListener(this.onResize)
|
||||
document.removeEventListener('scroll', this.onResize)
|
||||
removeScrollListener(this.onResize)
|
||||
},
|
||||
methods: {
|
||||
observe,
|
||||
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
})
|
||||
registerResizeListener(this.onResize)
|
||||
document.addEventListener('scroll', this.onResize)
|
||||
addScrollListener(this.onResize)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,25 +1,49 @@
|
|||
import {
|
||||
accountProfileName,
|
||||
closeDialogButton,
|
||||
composeInput,
|
||||
getNthAutosuggestionResult,
|
||||
getNthStatusMediaButton, getNthStatusSensitiveMediaButton,
|
||||
getNthStatus,
|
||||
getNumSyntheticListeners,
|
||||
getUrl,
|
||||
homeNavButton,
|
||||
homeNavButton, modalDialog, notificationsNavButton,
|
||||
scrollToStatus,
|
||||
scrollToTop,
|
||||
settingsNavButton, sleep
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { installDomListenerListener, getNumDomListeners } from '../spyDomListeners'
|
||||
import { homeTimeline } from '../fixtures'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
||||
fixture`038-memory-leaks.js`
|
||||
.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
|
||||
.click(settingsNavButton)
|
||||
.expect(getUrl()).contains('/settings')
|
||||
}
|
||||
|
||||
async function interactAndGoToEndPoint (t) {
|
||||
async function scrollUpAndDownAndDoAutosuggest (t) {
|
||||
await t
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
|
@ -28,19 +52,57 @@ async function interactAndGoToEndPoint (t) {
|
|||
await t
|
||||
.typeText(composeInput, 'hey @qu')
|
||||
.expect(getNthAutosuggestionResult(1).find('.sr-only').innerText).contains('@quux')
|
||||
.click(settingsNavButton)
|
||||
.expect(getUrl()).contains('/settings')
|
||||
await goToSettings(t)
|
||||
}
|
||||
|
||||
test('Does not leak synthetic listeners', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await goToStartPoint(t)
|
||||
await sleep(1000)
|
||||
const numSyntheticListeners = await getNumSyntheticListeners()
|
||||
async function openAndCloseMediaModal (t) {
|
||||
await t
|
||||
.expect(numSyntheticListeners).typeOf('number')
|
||||
await interactAndGoToEndPoint(t)
|
||||
await sleep(1000)
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
const idx = homeTimeline.findIndex(_ => _.spoiler === 'kitten CW')
|
||||
await scrollToStatus(t, idx + 1)
|
||||
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