mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
297 lines
15 KiB
JavaScript
297 lines
15 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import fs from 'node:fs'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const frozen = fs.readFileSync('refactor/baselines/site-vendor/console-original.js')
|
|
expect(createHash('sha256').update(frozen).digest('hex')).toBe('e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03')
|
|
async function setup(page, testInfo, historical) {
|
|
await page.route('**/site-console-fixture', route => route.fulfill({
|
|
contentType: 'text/html; charset=utf-8',
|
|
body: '<!doctype html><html><head><meta charset="utf-8"></head><body><div id="first" style="height:220px;width:640px"></div><div id="second" style="height:220px;width:640px"></div></body></html>',
|
|
}))
|
|
if (historical)
|
|
await page.route('**/assets/js/console.js', route => route.fulfill({ contentType: 'text/javascript', body: frozen }))
|
|
await page.goto('/site-console-fixture')
|
|
await page.evaluate(() => {
|
|
window.originalConsole = { ...window.console }
|
|
window.scrollTimers = new Set()
|
|
window.scrollFired = 0
|
|
const schedule = window.setTimeout
|
|
const cancel = window.clearTimeout
|
|
window.setTimeout = (callback, delay, ...args) => {
|
|
if (delay !== 200 || typeof callback !== 'function')
|
|
return schedule(callback, delay, ...args)
|
|
const id = schedule(() => {
|
|
window.scrollTimers.delete(id)
|
|
window.scrollFired++
|
|
Reflect.apply(callback, window, args)
|
|
}, delay)
|
|
window.scrollTimers.add(id)
|
|
return id
|
|
}
|
|
window.clearTimeout = (id) => {
|
|
window.scrollTimers.delete(id)
|
|
cancel(id)
|
|
}
|
|
})
|
|
await page.addScriptTag({ url: '/assets/js/console.js' })
|
|
await page.evaluate(() => {
|
|
window.viewer = window.consoleLog(document.querySelector('#first'))
|
|
})
|
|
await testInfo.attach('console-input', { contentType: 'application/json', body: JSON.stringify({
|
|
historical,
|
|
path: historical ? 'refactor/baselines/site-vendor/console-original.js' : 'docs/assets/js/console.js',
|
|
sha256: createHash('sha256').update(historical ? frozen : fs.readFileSync('docs/assets/js/console.js')).digest('hex'),
|
|
}) })
|
|
}
|
|
|
|
for (const historical of [false, true]) {
|
|
const scope = historical ? 'historical' : 'current'
|
|
test(`${scope} console globals, returned component and repeated mount retain their interfaces`, async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, historical)
|
|
expect(await page.evaluate(() => {
|
|
const first = document.querySelector('#first')
|
|
const hook = window.console.log
|
|
return {
|
|
react: window.React.version,
|
|
dom: window.ReactDOM.version,
|
|
loader: typeof window.parcelRequire,
|
|
entry: window.parcelRequire('Focm').default === window.consoleLog,
|
|
reactModule: window.parcelRequire('n8MK') === window.React,
|
|
sameComponent: window.consoleLog(first) === window.viewer,
|
|
sameHook: window.console.log === hook,
|
|
methods: ['add', 'onClear', 'onMouseEnter', 'onMouseLeave', 'componentWillUnmount', 'render'].every(name => typeof window.viewer[name] === 'function'),
|
|
state: { count: window.viewer.state.logs.length, hover: window.viewer.state.hover },
|
|
}
|
|
})).toEqual({ react: '17.0.2', dom: '17.0.2', loader: 'function', entry: true, reactModule: true, sameComponent: true, sameHook: true, methods: true, state: { count: 0, hover: false } })
|
|
await expect(page.locator('.console-header-number')).toHaveText('0')
|
|
expect(await page.evaluate(() => window.ReactDOM.unmountComponentAtNode(document.querySelector('#first')))).toBe(true)
|
|
expect(await page.evaluate(() => ({ restored: window.console.log === window.originalConsole.log, feed: 'feed' in window.console }))).toEqual({ restored: true, feed: false })
|
|
})
|
|
|
|
test(`${scope} console renders levels and raw objects; Clear resets the visible log count`, async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, historical)
|
|
await page.evaluate(() => {
|
|
window.sample = { message: 'structured-value', nested: { answer: 42 } }
|
|
window.sample.self = window.sample
|
|
window.console.log('plain-message', window.sample)
|
|
window.console.info('info-message')
|
|
window.console.warn('warning-message')
|
|
window.console.error('error-message', new Error('native-error'))
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('4')
|
|
await expect(page.locator('.console-component')).toContainText('plain-message')
|
|
await expect(page.locator('.console-component')).toContainText('warning-message')
|
|
await expect(page.locator('.console-component')).toContainText('error-message')
|
|
expect(await page.evaluate(() => ({ methods: window.viewer.state.logs.map(log => log.method), objectIdentity: window.viewer.state.logs[0].data[1] === window.sample }))).toEqual({ methods: ['log', 'info', 'warn', 'error'], objectIdentity: true })
|
|
await page.locator('.console-header-right').click()
|
|
await expect(page.locator('.console-header-number')).toHaveText('0')
|
|
await expect(page.locator('.console-component')).toHaveText('')
|
|
})
|
|
|
|
test(`${scope} console scrolls new logs and preserves the pointer hover pause`, async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, historical)
|
|
await page.mouse.move(900, 600)
|
|
await page.evaluate(() => {
|
|
for (let i = 0; i < 40; i++)
|
|
window.console.log(`scroll-row-${i}`)
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('40')
|
|
await page.waitForFunction(() => window.scrollFired > 0)
|
|
await expect.poll(() => page.locator('.console-component').evaluate(node => node.scrollTop)).toBeGreaterThan(0)
|
|
await page.locator('.console-component').hover()
|
|
await page.waitForFunction(() => window.viewer.state.hover)
|
|
await page.evaluate(() => {
|
|
const node = document.querySelector('.console-component')
|
|
node.style.scrollBehavior = 'auto'
|
|
node.scrollTop = 0
|
|
window.console.log('hover-retains-position')
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('41')
|
|
expect(await page.evaluate(() => ({ top: document.querySelector('.console-component').scrollTop, timers: window.scrollTimers.size }))).toEqual({ top: 0, timers: 0 })
|
|
await page.mouse.move(900, 600)
|
|
await page.waitForFunction(() => !window.viewer.state.hover)
|
|
await page.evaluate(() => window.console.log('resume-scrolling'))
|
|
await expect(page.locator('.console-header-number')).toHaveText('42')
|
|
await expect.poll(() => page.locator('.console-component').evaluate(node => node.scrollTop)).toBeGreaterThan(0)
|
|
})
|
|
}
|
|
|
|
test('frozen historical console leaves its scheduled scroll callback after unmount', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, true)
|
|
await page.evaluate(() => {
|
|
// Keep the native callback pending until after the synchronous unmount.
|
|
window.viewer.add({ method: 'log', data: ['pending-scroll'] })
|
|
window.pendingBeforeUnmount = window.scrollTimers.size
|
|
window.ReactDOM.unmountComponentAtNode(document.querySelector('#first'))
|
|
window.pendingAfterUnmount = window.scrollTimers.size
|
|
})
|
|
expect(await page.evaluate(() => ({ before: window.pendingBeforeUnmount, after: window.pendingAfterUnmount }))).toEqual({ before: 1, after: 1 })
|
|
await page.waitForFunction(() => window.scrollFired === 1)
|
|
await expect(page.locator('#first')).toBeEmpty()
|
|
})
|
|
|
|
test('frozen historical console loses the remaining viewer when another viewer unmounts', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, true)
|
|
await page.evaluate(() => {
|
|
window.otherViewer = window.consoleLog(document.querySelector('#second'))
|
|
window.console.log('both-mounted')
|
|
})
|
|
await expect(page.locator('#first .console-header-number')).toHaveText('1')
|
|
await expect(page.locator('#second .console-header-number')).toHaveText('1')
|
|
await page.evaluate(() => {
|
|
window.ReactDOM.unmountComponentAtNode(document.querySelector('#first'))
|
|
window.console.log('remaining-viewer-loses-log')
|
|
})
|
|
await page.waitForFunction(() => window.viewer.state.logs.length === 1 && !('feed' in window.console))
|
|
// Drain the same console-feed macrotask queue before inspecting the survivor.
|
|
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0)))
|
|
expect(await page.evaluate(() => window.otherViewer.state.logs.map(log => log.data[0]))).toEqual(['both-mounted'])
|
|
})
|
|
|
|
test('frozen historical console error-only rendering loses the message on Firefox and WebKit', async ({ page, browserName }, testInfo) => {
|
|
await setup(page, testInfo, true)
|
|
await page.evaluate(() => {
|
|
window.nativeError = new Error('native-error-message')
|
|
window.console.error(window.nativeError)
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('1')
|
|
const result = await page.evaluate(() => ({
|
|
parsed: window.viewer.state.logs[0].data[0],
|
|
message: window.nativeError.message,
|
|
stack: window.nativeError.stack,
|
|
text: document.querySelector('.console-component').textContent,
|
|
}))
|
|
expect(result.parsed).toBe(result.stack)
|
|
expect(result.message).toBe('native-error-message')
|
|
for (const line of result.stack.trim().split('\n'))
|
|
expect(result.text).toContain(line.trim())
|
|
if (browserName === 'chromium')
|
|
expect(result.text).toContain(result.message)
|
|
else
|
|
expect(result.text).not.toContain(result.message)
|
|
await testInfo.attach('historical-error-rendering', { contentType: 'application/json', body: JSON.stringify(result) })
|
|
})
|
|
|
|
test('candidate console cancels pending scroll and log work on unmount and survives remount', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, false)
|
|
expect(await page.evaluate(() => {
|
|
window.viewer.add({ method: 'log', data: ['pending-scroll'] })
|
|
window.console.log('queued-before-unmount')
|
|
window.ReactDOM.unmountComponentAtNode(document.querySelector('#first'))
|
|
const pending = window.scrollTimers.size
|
|
window.viewer = window.consoleLog(document.querySelector('#first'))
|
|
window.console.log('after-remount')
|
|
return pending
|
|
})).toBe(0)
|
|
await expect(page.locator('.console-header-number')).toHaveText('1')
|
|
await expect(page.locator('.console-component')).toHaveText('after-remount')
|
|
})
|
|
|
|
for (const removeFirst of [true, false]) {
|
|
test(`candidate console retains the surviving viewer when removing ${removeFirst ? 'first' : 'second'}`, async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, false)
|
|
await page.evaluate(() => {
|
|
window.otherViewer = window.consoleLog(document.querySelector('#second'))
|
|
window.console.log('both-mounted')
|
|
})
|
|
await expect(page.locator('#first .console-header-number')).toHaveText('1')
|
|
await expect(page.locator('#second .console-header-number')).toHaveText('1')
|
|
await page.evaluate((first) => {
|
|
window.ReactDOM.unmountComponentAtNode(document.querySelector(first ? '#first' : '#second'))
|
|
window.console.log('survivor-log')
|
|
}, removeFirst)
|
|
const remaining = removeFirst ? '#second' : '#first'
|
|
await expect(page.locator(`${remaining} .console-header-number`)).toHaveText('2')
|
|
await expect(page.locator(`${remaining} .console-component`)).toContainText('survivor-log')
|
|
expect(await page.evaluate((selector) => {
|
|
window.ReactDOM.unmountComponentAtNode(document.querySelector(selector))
|
|
return window.console.log === window.originalConsole.log
|
|
}, remaining)).toBe(true)
|
|
})
|
|
}
|
|
|
|
test('candidate console keeps native Error message and stack visible without modifying Error', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, false)
|
|
await page.evaluate(() => {
|
|
window.nativeError = new Error('candidate-error-message')
|
|
window.originalStack = window.nativeError.stack
|
|
window.console.error(window.nativeError)
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('1')
|
|
await expect(page.locator('.console-component')).toContainText('candidate-error-message')
|
|
expect(await page.evaluate(() => window.nativeError.stack === window.originalStack)).toBe(true)
|
|
const stack = await page.evaluate(() => window.originalStack)
|
|
for (const line of stack.trim().split('\n'))
|
|
await expect(page.locator('.console-component')).toContainText(line.trim())
|
|
})
|
|
|
|
test('candidate console leaves later external wrappers intact and remounts without duplicate forwarding', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, false)
|
|
await page.evaluate(() => {
|
|
const own = window.console.log
|
|
window.externalCalls = 0
|
|
window.external = function (...args) {
|
|
window.externalCalls++
|
|
return own.apply(this, args)
|
|
}
|
|
window.console.log = window.external
|
|
window.consoleLog.unmount(document.querySelector('#first'))
|
|
window.console.log('no-viewer')
|
|
window.viewer = window.consoleLog(document.querySelector('#first'))
|
|
window.console.log('remounted')
|
|
})
|
|
await expect(page.locator('.console-header-number')).toHaveText('1')
|
|
await expect(page.locator('.console-component')).toHaveText('remounted')
|
|
expect(await page.evaluate(() => {
|
|
window.consoleLog.unmount(document.querySelector('#first'))
|
|
return { calls: window.externalCalls, retained: window.console.log === window.external }
|
|
})).toEqual({ calls: 2, retained: true })
|
|
})
|
|
|
|
test('editor retains console on persisted pagehide and releases it on final pagehide', async ({ page }) => {
|
|
await page.route('https://**/*', route => route.fulfill({ status: 200, body: '' }))
|
|
await page.goto('/?code=window.console.log("editor-initial")')
|
|
await expect(page.locator('.console-component')).toContainText('editor-initial')
|
|
const before = await page.locator('.console-header-number').textContent()
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true }))
|
|
window.console.log('persisted-live')
|
|
})
|
|
await expect(page.locator('.console-component')).toContainText('persisted-live')
|
|
await expect(page.locator('.console-header-number')).toHaveText(String(Number(before) + 1))
|
|
await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: false })))
|
|
await expect(page.locator('.console-header')).toHaveCount(0)
|
|
expect(await page.evaluate(() => 'feed' in window.console)).toBe(false)
|
|
})
|
|
|
|
test('candidate console parses stateful methods once for shared viewers', async ({ page }, testInfo) => {
|
|
await setup(page, testInfo, false)
|
|
await page.evaluate(() => {
|
|
window.otherViewer = window.consoleLog(document.querySelector('#second'))
|
|
window.console.count('shared-count')
|
|
window.console.count('shared-count')
|
|
window.console.assert(true, 'must-not-log')
|
|
window.console.assert(false, 'visible-assertion')
|
|
window.console.time('shared-time')
|
|
window.console.timeEnd('shared-time')
|
|
})
|
|
await expect(page.locator('#first .console-header-number')).toHaveText('4')
|
|
await expect(page.locator('#second .console-header-number')).toHaveText('4')
|
|
const result = await page.evaluate(() => ({
|
|
a: window.viewer.state.logs,
|
|
b: window.otherViewer.state.logs,
|
|
sameRecords: window.viewer.state.logs.every((row, index) => row === window.otherViewer.state.logs[index]),
|
|
}))
|
|
expect(result.a).toEqual(result.b)
|
|
expect(result.sameRecords).toBe(true)
|
|
expect(JSON.stringify(result.a)).toContain('shared-count: 1')
|
|
expect(JSON.stringify(result.a)).toContain('shared-count: 2')
|
|
await expect(page.locator('#first .console-component')).toContainText('visible-assertion')
|
|
await expect(page.locator('#first .console-component')).not.toContainText('must-not-log')
|
|
await page.evaluate(() => window.console.clear())
|
|
await expect(page.locator('#first .console-header-number')).toHaveText('5')
|
|
await expect(page.locator('#second .console-header-number')).toHaveText('5')
|
|
expect(await page.evaluate(() => window.viewer.state.logs.at(-1).method)).toBe('clear')
|
|
})
|