184 lines
9.5 KiB
JavaScript
184 lines
9.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { createHash } from 'node:crypto'
|
|
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
|
const projectRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
|
const runtimeModule = join(projectRoot, '.tmp/rea-css-runtime/dist/browser/CdpElectronStyles.js')
|
|
const { captureElectronStyles, ingestElectronStyleEvent } = await import(pathToFileURL(runtimeModule))
|
|
const authorizedFrameIds = new Set(['frame-main'])
|
|
const noCompletenessWrites = { exclude() { assert.fail('CSS capture must not write into the old completeness sections') } }
|
|
const limitations = []
|
|
const styles = new Map()
|
|
const addStyle = (styleSheetId, header) => ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styles)
|
|
const removeStyle = styleSheetId => ingestElectronStyleEvent({ method: 'CSS.styleSheetRemoved', params: { styleSheetId } }, styles)
|
|
|
|
const fixtureRoot = await mkdtemp(join(tmpdir(), 'rea-css-fixtures-'))
|
|
const cssFile = join(fixtureRoot, 'authorized.css')
|
|
await writeFile(cssFile, 'body { color: red; }\n')
|
|
const cssUrl = pathToFileURL(cssFile).href
|
|
addStyle('file-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
|
addStyle('inline-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
|
addStyle('constructed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
|
addStyle('remote-style', { frameId: 'frame-main', sourceURL: 'https://example.invalid/site.css', origin: 'regular' })
|
|
addStyle('other-frame-style', { frameId: 'frame-other', sourceURL: cssUrl, origin: 'regular' })
|
|
addStyle('user-agent-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'user-agent' })
|
|
addStyle('injected-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'injected' })
|
|
addStyle('inspector-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'inspector' })
|
|
addStyle('unknown-origin-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'unknown' })
|
|
addStyle('unattributed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular' })
|
|
addStyle('removed-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
|
removeStyle('removed-style')
|
|
assert.equal(styles.has('removed-style'), false)
|
|
removeStyle('missing-style')
|
|
|
|
const bodyTexts = new Map([
|
|
['file-style', 'body { color: red; }\n'],
|
|
['inline-style', '.inline { display: block; }'],
|
|
['constructed-style', '.constructed { display: grid; }']
|
|
])
|
|
const calls = []
|
|
const connection = {
|
|
async send(method, params, sessionId, signal) {
|
|
calls.push({ method, params, sessionId, signal })
|
|
if (method !== 'CSS.getStyleSheetText') throw new Error(`Unexpected command: ${method}`)
|
|
return { text: bodyTexts.get(params.styleSheetId) }
|
|
}
|
|
}
|
|
const included = await captureElectronStyles({
|
|
connection,
|
|
sessionId: 'private-session-1',
|
|
signal: undefined,
|
|
styles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: true },
|
|
completeness: noCompletenessWrites,
|
|
limitations
|
|
})
|
|
assert.equal(included.total, 10)
|
|
assert.equal(included.excluded, 7)
|
|
assert.equal(included.source_failures, 0)
|
|
assert.equal(included.truncated, false)
|
|
assert.equal(included.items.length, 3)
|
|
assert.equal(calls.length, 3)
|
|
assert.ok(calls.every(call => call.method === 'CSS.getStyleSheetText' && call.sessionId === 'private-session-1'))
|
|
assert.deepEqual(included.limits, { max_sheets: 1024, max_sheet_bytes: 8 * 1024 * 1024, max_total_bytes: 64 * 1024 * 1024 })
|
|
const fileItem = included.items.find(item => item.style_sheet_id === 'file-style')
|
|
assert.equal(fileItem.file_path, await realpath(cssFile))
|
|
assert.equal(fileItem.source.included, true)
|
|
assert.equal(fileItem.source.artifact.media_type, 'text/css')
|
|
assert.equal(fileItem.source.artifact.text, bodyTexts.get('file-style'))
|
|
assert.equal(fileItem.source.artifact.bytes, Buffer.byteLength(bodyTexts.get('file-style'), 'utf8'))
|
|
assert.equal(fileItem.source.artifact.sha256, createHash('sha256').update(bodyTexts.get('file-style')).digest('hex'))
|
|
assert.match(fileItem.style_sheet_key, /^electron_stylesheet_[a-f0-9]{64}$/)
|
|
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').file_path, null)
|
|
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').is_inline, true)
|
|
assert.equal(included.items.find(item => item.style_sheet_id === 'constructed-style').is_constructed, true)
|
|
assert.doesNotMatch(JSON.stringify(included), /example\.invalid/)
|
|
|
|
const unselectedCalls = []
|
|
const unselected = await captureElectronStyles({
|
|
connection: { async send(...args) { unselectedCalls.push(args); throw new Error('CSS body must not be requested') } },
|
|
sessionId: 'private-session-1',
|
|
styles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: false },
|
|
completeness: noCompletenessWrites,
|
|
limitations: []
|
|
})
|
|
assert.equal(unselected.total, 10)
|
|
assert.equal(unselected.items.length, 0)
|
|
assert.equal(unselected.excluded, 7)
|
|
assert.equal(unselectedCalls.length, 0)
|
|
|
|
const failingStyles = new Map()
|
|
addStyleTo(failingStyles, 'throwing-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
|
addStyleTo(failingStyles, 'invalid-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
|
addStyleTo(failingStyles, 'surviving-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
|
const failures = await captureElectronStyles({
|
|
connection: { async send(_method, params) {
|
|
if (params.styleSheetId === 'throwing-style') throw new Error('stylesheet was removed')
|
|
if (params.styleSheetId === 'invalid-style') return { text: 3 }
|
|
return { text: '.still-captured{}' }
|
|
} },
|
|
sessionId: 'private-session-2',
|
|
styles: failingStyles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: true },
|
|
completeness: noCompletenessWrites,
|
|
limitations: []
|
|
})
|
|
assert.equal(failures.items.length, 3)
|
|
assert.equal(failures.source_failures, 2)
|
|
assert.equal(failures.truncated, false)
|
|
assert.equal(failures.items.find(item => item.style_sheet_id === 'throwing-style').source.reason, 'CSS.getStyleSheetText failed')
|
|
assert.equal(failures.items.find(item => item.style_sheet_id === 'invalid-style').source.reason, 'CSS.getStyleSheetText returned invalid text')
|
|
assert.equal(failures.items.find(item => item.style_sheet_id === 'surviving-style').source.included, true)
|
|
|
|
const oversizedStyles = new Map()
|
|
addStyleTo(oversizedStyles, 'oversized', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
|
const oversized = await captureElectronStyles({
|
|
connection: { async send() { return { text: 'x'.repeat(8 * 1024 * 1024 + 1) } } },
|
|
sessionId: 'private-session-3',
|
|
styles: oversizedStyles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: true },
|
|
completeness: noCompletenessWrites,
|
|
limitations: []
|
|
})
|
|
assert.equal(oversized.source_failures, 1)
|
|
assert.equal(oversized.truncated, true)
|
|
assert.equal(oversized.items[0].source.included, false)
|
|
assert.match(oversized.items[0].source.reason, /per-stylesheet byte limit/)
|
|
|
|
const aggregateStyles = new Map()
|
|
const aggregateText = 'a'.repeat(8 * 1024 * 1024)
|
|
for (let index = 0; index < 8; index += 1) {
|
|
addStyleTo(aggregateStyles, `aggregate-${String(index).padStart(2, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
|
}
|
|
addStyleTo(aggregateStyles, 'aggregate-overflow', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
|
const aggregate = await captureElectronStyles({
|
|
connection: { async send(_method, params) { return { text: params.styleSheetId === 'aggregate-overflow' ? 'overflow' : aggregateText } } },
|
|
sessionId: 'private-session-4',
|
|
styles: aggregateStyles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: true },
|
|
completeness: noCompletenessWrites,
|
|
limitations: []
|
|
})
|
|
assert.equal(aggregate.source_failures, 1)
|
|
assert.equal(aggregate.truncated, true)
|
|
assert.equal(aggregate.items.filter(item => item.source.included).length, 8)
|
|
assert.equal(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.included, false)
|
|
assert.match(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.reason, /aggregate stylesheet source byte limit/)
|
|
|
|
const countLimitedStyles = new Map()
|
|
for (let index = 0; index < 1025; index += 1) {
|
|
addStyleTo(countLimitedStyles, `sheet-${String(index).padStart(4, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
|
}
|
|
let countRequests = 0
|
|
const countLimited = await captureElectronStyles({
|
|
connection: { async send() { countRequests += 1; return { text: '' } } },
|
|
sessionId: 'private-session-5',
|
|
styles: countLimitedStyles,
|
|
frameIds: authorizedFrameIds,
|
|
request: { include_style_sources: true },
|
|
completeness: noCompletenessWrites,
|
|
limitations: []
|
|
})
|
|
assert.equal(countLimited.total, 1025)
|
|
assert.equal(countLimited.items.length, 1025)
|
|
assert.equal(countLimited.source_failures, 1)
|
|
assert.equal(countLimited.truncated, true)
|
|
assert.equal(countRequests, 1024)
|
|
assert.equal(countLimited.items.filter(item => item.source.included).length, 1024)
|
|
|
|
await rm(fixtureRoot, { recursive: true, force: true })
|
|
console.log('REA Electron CSS capture regression checks passed')
|
|
|
|
function addStyleTo(styleMap, styleSheetId, header) {
|
|
ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styleMap)
|
|
}
|