Skip to content

Commit 0037995

Browse files
committed
fix: scope visible error mutation scans
1 parent bcc21cf commit 0037995

2 files changed

Lines changed: 81 additions & 25 deletions

File tree

browser_tests/fixtures/utils/errorSurfaces.ts

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,55 @@ function installVisibleErrorRecorder(
3636
const seen = new Set<string>()
3737
const errors: VisibleError[] = []
3838
target.__cnVisibleErrors = errors
39-
const sample = () => {
39+
const record = (surface: string, element: Element) => {
40+
if (
41+
!(element instanceof HTMLElement) ||
42+
!element.checkVisibility({
43+
checkOpacity: true,
44+
checkVisibilityCSS: true
45+
})
46+
)
47+
return
48+
const text = (element.innerText || element.textContent || '')
49+
.replace(/\s+/g, ' ')
50+
.trim()
51+
.slice(0, 300)
52+
const key = `${surface}\u0000${text}`
53+
if (seen.has(key)) return
54+
seen.add(key)
55+
errors.push({ surface, text })
56+
}
57+
const sampleElement = (element: Element, includeDescendants: boolean) => {
4058
for (const { surface, selector } of selectors) {
41-
for (const element of document.querySelectorAll(selector)) {
42-
if (
43-
!(element instanceof HTMLElement) ||
44-
!element.checkVisibility({
45-
checkOpacity: true,
46-
checkVisibilityCSS: true
47-
})
48-
)
49-
continue
50-
const text = (element.innerText || element.textContent || '')
51-
.replace(/\s+/g, ' ')
52-
.trim()
53-
.slice(0, 300)
54-
const key = `${surface}\u0000${text}`
55-
if (seen.has(key)) continue
56-
seen.add(key)
57-
errors.push({ surface, text })
58-
}
59+
const closest = element.closest(selector)
60+
if (closest) record(surface, closest)
61+
if (!includeDescendants) continue
62+
for (const descendant of element.querySelectorAll(selector))
63+
record(surface, descendant)
5964
}
6065
}
61-
sample()
62-
new MutationObserver(sample).observe(document, {
66+
for (const { surface, selector } of selectors)
67+
for (const element of document.querySelectorAll(selector))
68+
record(surface, element)
69+
new MutationObserver((mutations) => {
70+
const exact = new Set<Element>()
71+
const subtrees = new Set<Element>()
72+
for (const mutation of mutations) {
73+
if (mutation.type === 'childList') {
74+
if (mutation.target instanceof Element) exact.add(mutation.target)
75+
for (const node of mutation.addedNodes) {
76+
if (node instanceof Element) subtrees.add(node)
77+
else if (node.parentElement) exact.add(node.parentElement)
78+
}
79+
} else if (mutation.target instanceof Element) {
80+
subtrees.add(mutation.target)
81+
} else if (mutation.target.parentElement) {
82+
exact.add(mutation.target.parentElement)
83+
}
84+
}
85+
for (const element of exact) sampleElement(element, false)
86+
for (const element of subtrees) sampleElement(element, true)
87+
}).observe(document, {
6388
attributes: true,
6489
characterData: true,
6590
childList: true,

browser_tests/tests/customNodes/errorSurfaces.pure.spec.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ test('a closed page fails immediately with the real reason, not the sentinel', a
5151
test('a visible error toast fails after it clears before the assertion', async ({
5252
page
5353
}) => {
54-
const html =
55-
'<div id="t" class="p-toast-message-error">momentary</div>' +
56-
'<script>setTimeout(() => document.getElementById("t").remove(), 800)</script>'
57-
await page.goto(`data:text/html,${encodeURIComponent(html)}`)
54+
await page.setContent('<div id="t">momentary</div>')
55+
await page.evaluate(() => {
56+
const toast = document.getElementById('t')!
57+
toast.classList.add('p-toast-message-error')
58+
setTimeout(() => toast.remove(), 800)
59+
})
5860
await expect(page.locator('#t')).toHaveCount(0)
5961
const failure = await expectNoVisibleErrors(page, 'transient').then(
6062
() => undefined,
@@ -63,3 +65,32 @@ test('a visible error toast fails after it clears before the assertion', async (
6365
expect(failure).toBeInstanceOf(Error)
6466
expect(String(failure)).toContain('momentary')
6567
})
68+
69+
test('unrelated mutations do not rescan the full document', async ({
70+
page
71+
}) => {
72+
await page.setContent('<main id="root"></main>')
73+
const fullDocumentQueries = await page.evaluate(async () => {
74+
const original = document.querySelectorAll.bind(document)
75+
let calls = 0
76+
document.querySelectorAll = ((selector: string) => {
77+
calls += 1
78+
return original(selector)
79+
}) as typeof document.querySelectorAll
80+
const root = document.getElementById('root')!
81+
for (let index = 0; index < 1_000; index += 1) {
82+
const child = document.createElement('span')
83+
root.append(child)
84+
child.dataset.index = String(index)
85+
child.textContent = String(index)
86+
await Promise.resolve()
87+
}
88+
await new Promise((resolve) => setTimeout(resolve, 0))
89+
return calls
90+
})
91+
92+
expect(fullDocumentQueries).toBe(0)
93+
await expect(
94+
expectNoVisibleErrors(page, 'after DOM churn')
95+
).resolves.toBeUndefined()
96+
})

0 commit comments

Comments
 (0)