Added Hammerjs memory test.
This commit is contained in:
parent
74b5d69389
commit
7f5e0e8e79
@ -9,6 +9,8 @@
|
||||
"scripts": {
|
||||
"test": "node bin/testrunner.js",
|
||||
"test-eventlistener": "node ./test/tests/eventlistener/index.js",
|
||||
"test-eventlistener-remove": "node ./test/tests/eventlistener/remove.js",
|
||||
"test-eventlistener-hammerjs": "node ./test/tests/eventlistener/hammerjs.js",
|
||||
"build": "rollup --config ./rollup.config.js",
|
||||
"watch": "rollup --watch --config ./rollup.config.js",
|
||||
"3rdparty": "gulp",
|
||||
|
File diff suppressed because one or more lines are too long
92
test/tests/eventlistener/hammerjs.html
Normal file
92
test/tests/eventlistener/hammerjs.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Test EventListener</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css" />
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.0.18/chance.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Memory Test EventListener with HammerJS
|
||||
</h1>
|
||||
<p class="subtitle">Test EventListener in <strong>dynamic content</strong>!</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="buttons">
|
||||
<a class="button is-info is-fullwidth" id="add">Add HTML content</a>
|
||||
<a class="button is-danger is-fullwidth" id="delete">Delete new content</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column" id="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const html = `
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<figure class="image is-4by3">
|
||||
<img src="./images/1280x960.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img src="./images/96x96.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">John Smith</p>
|
||||
<p class="subtitle is-6">@johnsmith</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Phasellus nec iaculis mauris.
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a href="#" class="card-footer-item" id="contentRandom">Random</a>
|
||||
<a href="#" class="card-footer-item" id="contentDelete">Delete</a>
|
||||
</footer>
|
||||
</div>
|
||||
`
|
||||
|
||||
const hammerAdd = new Hammer(document.querySelector('#add'))
|
||||
hammerAdd.on('tap', function(event) {
|
||||
document.querySelector('#content').innerHTML = html
|
||||
|
||||
const hammerContentRandom = new Hammer(document.querySelector('#contentRandom'))
|
||||
hammerContentRandom.on('tap', function() {
|
||||
document.querySelector('.media-content .title').innerHTML = chance.name()
|
||||
document.querySelector('.media-content .subtitle').innerHTML = chance.email()
|
||||
document.querySelector('.card-content .content').innerHTML = chance.sentence({ words: 3 })
|
||||
})
|
||||
|
||||
const hammerContentDelete = new Hammer(document.querySelector('#contentDelete'))
|
||||
hammerContentDelete.on('tap', function() {
|
||||
document.querySelector('#content').innerHTML = ''
|
||||
})
|
||||
})
|
||||
|
||||
const hammerDelete = new Hammer(document.querySelector('#delete'))
|
||||
hammerDelete.on('tap', function(event) {
|
||||
document.querySelector('#content').innerHTML = ''
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
155
test/tests/eventlistener/hammerjs.js
Normal file
155
test/tests/eventlistener/hammerjs.js
Normal file
@ -0,0 +1,155 @@
|
||||
/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */
|
||||
|
||||
const puppeteer = require('puppeteer')
|
||||
const fse = require('fs-extra')
|
||||
const _ = require('lodash')
|
||||
|
||||
const CYCLES = 5000
|
||||
const TIMEOUT = 250
|
||||
|
||||
;(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: ['--disable-web-security'],
|
||||
defaultViewport: {
|
||||
width: 1920,
|
||||
height: 1280,
|
||||
hasTouch: true
|
||||
}
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
await page.goto(`file://${__dirname}/hammerjs.html`)
|
||||
|
||||
console.log('App loaded')
|
||||
|
||||
const metrics = []
|
||||
|
||||
for (let i = 0; i < CYCLES; i++) {
|
||||
console.log(`Cycle ${i + 1} of ${CYCLES}`)
|
||||
|
||||
await sleep(TIMEOUT)
|
||||
metrics.push(await page.metrics())
|
||||
|
||||
await page.tap('#add') // button 1
|
||||
await sleep(100)
|
||||
for (let j = 0; j < 10; j++) {
|
||||
await page.tap('#contentRandom') // button in card
|
||||
}
|
||||
await sleep(100)
|
||||
await page.tap('#delete') // button 2
|
||||
}
|
||||
|
||||
await writeMetrics(metrics)
|
||||
|
||||
await page.setViewport({
|
||||
width: 1920,
|
||||
height: 1280,
|
||||
deviceScaleFactor: 1
|
||||
})
|
||||
|
||||
await page.goto(`file://${__dirname}/../../chart/index.html`)
|
||||
|
||||
//await browser.close()
|
||||
})()
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
async function writeMetrics(metrics) {
|
||||
const first = metrics[0].Timestamp
|
||||
const timestamp = metrics.map(it => _.round(it.Timestamp - first, 1))
|
||||
const documents = metrics.map(it => it.Documents)
|
||||
const frames = metrics.map(it => it.Frames)
|
||||
const jsEventListeners = metrics.map(it => it.JSEventListeners)
|
||||
const nodes = metrics.map(it => it.Nodes)
|
||||
const layoutCount = metrics.map(it => it.LayoutCount)
|
||||
const recalcStyleCount = metrics.map(it => it.RecalcStyleCount)
|
||||
const layoutDuration = metrics.map(it => it.LayoutDuration)
|
||||
const recalcStyleDuration = metrics.map(it => it.RecalcStyleDuration)
|
||||
const scriptDuration = metrics.map(it => it.ScriptDuration)
|
||||
const taskDuration = metrics.map(it => it.TaskDuration)
|
||||
const jsHeapUsedSize = metrics.map(it => it.JSHeapUsedSize / 1024 / 1024)
|
||||
const jsHeapTotalSize = metrics.map(it => it.JSHeapTotalSize / 1024 / 1024)
|
||||
|
||||
const labels = `[${timestamp.join(', ')}]`
|
||||
|
||||
await fse.outputFile(
|
||||
`${__dirname}/../../chart/data.js`,
|
||||
`
|
||||
var data = [{
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'Documents',
|
||||
backgroundColor: 'rgba(205, 37, 44, 0.2)',
|
||||
borderColor: 'rgba(205, 37, 44, 1.00)',
|
||||
data: [${documents.join(', ')}]
|
||||
}, {
|
||||
label: 'Frames',
|
||||
backgroundColor: 'rgba(239, 116, 55, 0.2)',
|
||||
borderColor: 'rgba(239, 116, 55, 1.00)',
|
||||
data: [${frames.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'JSEventListeners',
|
||||
backgroundColor: 'rgba(248, 189, 64, 0.2)',
|
||||
borderColor: 'rgba(248, 189, 64, 1.00)',
|
||||
data: [${jsEventListeners.join(', ')}]
|
||||
}, {
|
||||
label: 'Nodes',
|
||||
backgroundColor: 'rgba(181, 202, 62, 0.2)',
|
||||
borderColor: 'rgba(181, 202, 62, 1.00)',
|
||||
data: [${nodes.join(', ')}]
|
||||
}, {
|
||||
label: 'LayoutCount',
|
||||
backgroundColor: 'rgba(50, 184, 79, 0.2)',
|
||||
borderColor: 'rgba(50, 184, 79, 1.00)',
|
||||
data: [${layoutCount.join(', ')}]
|
||||
}, {
|
||||
label: 'RecalcStyleCount',
|
||||
backgroundColor: 'rgba(37, 180, 171, 0.2)',
|
||||
borderColor: 'rgba(37, 180, 171, 1.00)',
|
||||
data: [${recalcStyleCount.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'LayoutDuration',
|
||||
backgroundColor: 'rgba(45, 134, 203, 0.2)',
|
||||
borderColor: 'rgba(45, 134, 203, 1.00)',
|
||||
data: [${layoutDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'RecalcStyleDuration',
|
||||
backgroundColor: 'rgba(100, 58, 195, 0.2)',
|
||||
borderColor: 'rgba(100, 58, 195, 1.00)',
|
||||
data: [${recalcStyleDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'ScriptDuration',
|
||||
backgroundColor: 'rgba(161, 59, 195, 0.2)',
|
||||
borderColor: 'rgba(161, 59, 195, 1.00)',
|
||||
data: [${scriptDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'TaksDuration',
|
||||
backgroundColor: 'rgba(221, 65, 150, 0.2)',
|
||||
borderColor: 'rgba(221, 65, 150, 1.00)',
|
||||
data: [${taskDuration.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'JSHeapUsedSize',
|
||||
backgroundColor: 'rgba(163, 104, 70, 0.2)',
|
||||
borderColor: 'rgba(163, 104, 70, 1.00)',
|
||||
data: [${jsHeapUsedSize.join(', ')}]
|
||||
}, {
|
||||
label: 'JSHeapTotalSize',
|
||||
backgroundColor: 'rgba(118, 118, 118, 0.2)',
|
||||
borderColor: 'rgba(118, 118, 118, 1.00)',
|
||||
data: [${jsHeapTotalSize.join(', ')}]
|
||||
}]
|
||||
}]
|
||||
`
|
||||
)
|
||||
}
|
BIN
test/tests/eventlistener/images/1280x960.png
Normal file
BIN
test/tests/eventlistener/images/1280x960.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
test/tests/eventlistener/images/96x96.png
Normal file
BIN
test/tests/eventlistener/images/96x96.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -37,14 +37,14 @@
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<figure class="image is-4by3">
|
||||
<img src="https://bulma.io/images/placeholders/1280x960.png" alt="Placeholder image">
|
||||
<img src="./images/1280x960.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img src="https://bulma.io/images/placeholders/96x96.png" alt="Placeholder image">
|
||||
<img src="./images/96x96.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
|
@ -4,7 +4,7 @@ const puppeteer = require('puppeteer')
|
||||
const fse = require('fs-extra')
|
||||
const _ = require('lodash')
|
||||
|
||||
const CYCLES = 50
|
||||
const CYCLES = 5000
|
||||
const TIMEOUT = 250
|
||||
|
||||
;(async () => {
|
||||
|
91
test/tests/eventlistener/remove.html
Normal file
91
test/tests/eventlistener/remove.html
Normal file
@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Test EventListener</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css" />
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.0.18/chance.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Memory Test EventListener with removal
|
||||
</h1>
|
||||
<p class="subtitle">Test EventListener in <strong>dynamic content</strong>!</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="buttons">
|
||||
<a class="button is-info is-fullwidth" id="add">Add HTML content</a>
|
||||
<a class="button is-danger is-fullwidth" id="delete">Delete new content</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column" id="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const html = `
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<figure class="image is-4by3">
|
||||
<img src="./images/1280x960.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img src="./images/96x96.png" alt="Placeholder image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">John Smith</p>
|
||||
<p class="subtitle is-6">@johnsmith</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Phasellus nec iaculis mauris.
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a href="#" class="card-footer-item" id="contentRandom">Random</a>
|
||||
<a href="#" class="card-footer-item" id="contentDelete">Delete</a>
|
||||
</footer>
|
||||
</div>
|
||||
`
|
||||
|
||||
function clickContentRandom() {
|
||||
document.querySelector('.media-content .title').innerHTML = chance.name()
|
||||
document.querySelector('.media-content .subtitle').innerHTML = chance.email()
|
||||
document.querySelector('.card-content .content').innerHTML = chance.sentence({ words: 3 })
|
||||
}
|
||||
|
||||
function clickContentDelete() {
|
||||
document.querySelector('#content').innerHTML = ''
|
||||
}
|
||||
|
||||
document.querySelector('#add').addEventListener('click', function() {
|
||||
document.querySelector('#content').innerHTML = html
|
||||
document.querySelector('#contentRandom').addEventListener('click', clickContentRandom, false)
|
||||
document.querySelector('#contentDelete').addEventListener('click', clickContentDelete, false)
|
||||
}, false)
|
||||
|
||||
document.querySelector('#delete').addEventListener('click', function() {
|
||||
document.querySelector('#contentRandom').removeEventListener('click', clickContentRandom, false)
|
||||
document.querySelector('#contentDelete').removeEventListener('click', clickContentDelete, false)
|
||||
document.querySelector('#content').innerHTML = ''
|
||||
}, false)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
155
test/tests/eventlistener/remove.js
Normal file
155
test/tests/eventlistener/remove.js
Normal file
@ -0,0 +1,155 @@
|
||||
/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */
|
||||
|
||||
const puppeteer = require('puppeteer')
|
||||
const fse = require('fs-extra')
|
||||
const _ = require('lodash')
|
||||
|
||||
const CYCLES = 5000
|
||||
const TIMEOUT = 250
|
||||
|
||||
;(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: ['--disable-web-security'],
|
||||
defaultViewport: {
|
||||
width: 1920,
|
||||
height: 1280,
|
||||
hasTouch: false
|
||||
}
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
await page.goto(`file://${__dirname}/remove.html`)
|
||||
|
||||
console.log('App loaded')
|
||||
|
||||
const metrics = []
|
||||
|
||||
for (let i = 0; i < CYCLES; i++) {
|
||||
console.log(`Cycle ${i + 1} of ${CYCLES}`)
|
||||
|
||||
await sleep(TIMEOUT)
|
||||
metrics.push(await page.metrics())
|
||||
|
||||
await page.click('#add') // button 1
|
||||
await sleep(100)
|
||||
for (let j = 0; j < 10; j++) {
|
||||
await page.click('#contentRandom') // button in card
|
||||
}
|
||||
await sleep(100)
|
||||
await page.click('#delete') // button 2
|
||||
}
|
||||
|
||||
await writeMetrics(metrics)
|
||||
|
||||
await page.setViewport({
|
||||
width: 1920,
|
||||
height: 1280,
|
||||
deviceScaleFactor: 1
|
||||
})
|
||||
|
||||
await page.goto(`file://${__dirname}/../../chart/index.html`)
|
||||
|
||||
//await browser.close()
|
||||
})()
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
async function writeMetrics(metrics) {
|
||||
const first = metrics[0].Timestamp
|
||||
const timestamp = metrics.map(it => _.round(it.Timestamp - first, 1))
|
||||
const documents = metrics.map(it => it.Documents)
|
||||
const frames = metrics.map(it => it.Frames)
|
||||
const jsEventListeners = metrics.map(it => it.JSEventListeners)
|
||||
const nodes = metrics.map(it => it.Nodes)
|
||||
const layoutCount = metrics.map(it => it.LayoutCount)
|
||||
const recalcStyleCount = metrics.map(it => it.RecalcStyleCount)
|
||||
const layoutDuration = metrics.map(it => it.LayoutDuration)
|
||||
const recalcStyleDuration = metrics.map(it => it.RecalcStyleDuration)
|
||||
const scriptDuration = metrics.map(it => it.ScriptDuration)
|
||||
const taskDuration = metrics.map(it => it.TaskDuration)
|
||||
const jsHeapUsedSize = metrics.map(it => it.JSHeapUsedSize / 1024 / 1024)
|
||||
const jsHeapTotalSize = metrics.map(it => it.JSHeapTotalSize / 1024 / 1024)
|
||||
|
||||
const labels = `[${timestamp.join(', ')}]`
|
||||
|
||||
await fse.outputFile(
|
||||
`${__dirname}/../../chart/data.js`,
|
||||
`
|
||||
var data = [{
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'Documents',
|
||||
backgroundColor: 'rgba(205, 37, 44, 0.2)',
|
||||
borderColor: 'rgba(205, 37, 44, 1.00)',
|
||||
data: [${documents.join(', ')}]
|
||||
}, {
|
||||
label: 'Frames',
|
||||
backgroundColor: 'rgba(239, 116, 55, 0.2)',
|
||||
borderColor: 'rgba(239, 116, 55, 1.00)',
|
||||
data: [${frames.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'JSEventListeners',
|
||||
backgroundColor: 'rgba(248, 189, 64, 0.2)',
|
||||
borderColor: 'rgba(248, 189, 64, 1.00)',
|
||||
data: [${jsEventListeners.join(', ')}]
|
||||
}, {
|
||||
label: 'Nodes',
|
||||
backgroundColor: 'rgba(181, 202, 62, 0.2)',
|
||||
borderColor: 'rgba(181, 202, 62, 1.00)',
|
||||
data: [${nodes.join(', ')}]
|
||||
}, {
|
||||
label: 'LayoutCount',
|
||||
backgroundColor: 'rgba(50, 184, 79, 0.2)',
|
||||
borderColor: 'rgba(50, 184, 79, 1.00)',
|
||||
data: [${layoutCount.join(', ')}]
|
||||
}, {
|
||||
label: 'RecalcStyleCount',
|
||||
backgroundColor: 'rgba(37, 180, 171, 0.2)',
|
||||
borderColor: 'rgba(37, 180, 171, 1.00)',
|
||||
data: [${recalcStyleCount.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'LayoutDuration',
|
||||
backgroundColor: 'rgba(45, 134, 203, 0.2)',
|
||||
borderColor: 'rgba(45, 134, 203, 1.00)',
|
||||
data: [${layoutDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'RecalcStyleDuration',
|
||||
backgroundColor: 'rgba(100, 58, 195, 0.2)',
|
||||
borderColor: 'rgba(100, 58, 195, 1.00)',
|
||||
data: [${recalcStyleDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'ScriptDuration',
|
||||
backgroundColor: 'rgba(161, 59, 195, 0.2)',
|
||||
borderColor: 'rgba(161, 59, 195, 1.00)',
|
||||
data: [${scriptDuration.join(', ')}]
|
||||
}, {
|
||||
label: 'TaksDuration',
|
||||
backgroundColor: 'rgba(221, 65, 150, 0.2)',
|
||||
borderColor: 'rgba(221, 65, 150, 1.00)',
|
||||
data: [${taskDuration.join(', ')}]
|
||||
}]
|
||||
}, {
|
||||
labels: ${labels},
|
||||
datasets: [{
|
||||
label: 'JSHeapUsedSize',
|
||||
backgroundColor: 'rgba(163, 104, 70, 0.2)',
|
||||
borderColor: 'rgba(163, 104, 70, 1.00)',
|
||||
data: [${jsHeapUsedSize.join(', ')}]
|
||||
}, {
|
||||
label: 'JSHeapTotalSize',
|
||||
backgroundColor: 'rgba(118, 118, 118, 0.2)',
|
||||
borderColor: 'rgba(118, 118, 118, 1.00)',
|
||||
data: [${jsHeapTotalSize.join(', ')}]
|
||||
}]
|
||||
}]
|
||||
`
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user