|
type ComponentType = |
|
| 'leafComponent' |
|
| 'utilComponent' |
|
| 'nonDsComponent' |
|
| 'layoutComponent' |
|
| 'rebrandComponent' |
|
| 'outdatedComponent' |
|
| 'unknownDsComponent' |
|
type EmptyPixelMarker = ' ' |
|
type NonDsComponentPixelMarker = '_' |
|
type DsComponentPixelMarkers = 'A' | 'U' | 'O' | 'L' | 'K' | 'R' |
|
type ComponentPixelMarkers = NonDsComponentPixelMarker | DsComponentPixelMarkers |
|
type PixelMarkers = EmptyPixelMarker | ComponentPixelMarkers |
|
type PixelCounts = Record<ComponentType, number> |
|
type RgbColor = string |
|
type Rect = Pick<DOMRect, 'top' | 'left' | 'width' | 'height'> |
|
type Milliseconds = number |
|
type Duration = { |
|
totalTime: Milliseconds |
|
countPixelsTime: Milliseconds, |
|
addBoundingRectTime: Milliseconds, |
|
loopOverDomChildrenTime: Milliseconds, |
|
} |
|
type Result = { |
|
done: true, |
|
href: string, |
|
duration: Duration |
|
pixelCounts: PixelCounts, |
|
dsCoverageVersion: string, |
|
viewportPixels: ViewportPixels, |
|
} | { |
|
done: false, |
|
} |
|
type Results = Record<string, Result> |
|
|
|
// virtual representation of the pixels of the page |
|
type ViewportPixels = Array<Array<PixelMarkers>> |
|
; (() => { |
|
const fakeRect: Rect = { top: -1, left: -1, width: -1, height: -1 } |
|
const emptyPixel: EmptyPixelMarker = ' ' // useful to print out the array with monospaced fonts |
|
const pixelMarkerByComponentType: Record<ComponentType, PixelMarkers> = { |
|
leafComponent: 'L', |
|
utilComponent: 'U', |
|
nonDsComponent: '_', |
|
layoutComponent: 'A', |
|
rebrandComponent: 'R', |
|
outdatedComponent: 'O', |
|
unknownDsComponent: 'K', |
|
} |
|
const colorByComponentType: Record<ComponentType, RgbColor> = { |
|
leafComponent: '#00FF00', |
|
utilComponent: '#00FF00', |
|
nonDsComponent: '#FF0000', |
|
layoutComponent: '#00FF00', |
|
rebrandComponent: '#0000FF', |
|
outdatedComponent: '#00FF00', |
|
unknownDsComponent: '#00FF00', |
|
} |
|
const componentTypeByPixelMarker: Record<ComponentPixelMarkers, ComponentType> = { |
|
_: 'nonDsComponent', |
|
|
|
L: 'leafComponent', |
|
U: 'utilComponent', |
|
A: 'layoutComponent', |
|
R: 'rebrandComponent', |
|
O: 'outdatedComponent', |
|
K: 'unknownDsComponent', |
|
} |
|
|
|
// TODO: type componentName with the real names to be prompted in case of future name additions |
|
function getDsComponentType(componentName: string | null): ComponentType { |
|
if (componentName === null) return 'nonDsComponent' |
|
|
|
if (componentName.startsWith('Rebrand')) { |
|
return 'rebrandComponent' |
|
} |
|
|
|
switch (componentName) { |
|
case 'Box': |
|
case 'LayoutFlex': |
|
case 'LayoutFlexItem': |
|
case 'LAYOUT_GRID': |
|
case 'LayoutGrid': |
|
case 'LayoutGridItem': |
|
return 'layoutComponent' |
|
|
|
case 'ObserverIntersection': |
|
return 'utilComponent' |
|
|
|
case 'Panel': |
|
return 'outdatedComponent' |
|
|
|
case 'Avatar': |
|
case 'AvatarWithStatus': |
|
case 'Badge': |
|
case 'Button': |
|
case 'FieldButton': |
|
case 'FieldLayout': |
|
case 'Heading': |
|
case 'Icon': |
|
case 'IconButton': |
|
case 'Link': |
|
case 'Loader': |
|
case 'NumberField': |
|
case 'PasswordField': |
|
case 'PreplyLogo': |
|
case 'SelectField': |
|
case 'Text': |
|
case 'TextField': |
|
case 'TextHighlighted': |
|
case 'TextInline': |
|
case 'TextareaField': |
|
case 'Checkbox': |
|
case 'InputDate': |
|
case 'InputNumber': |
|
case 'InputPassword': |
|
case 'InputText': |
|
case 'InputTime': |
|
case 'Radio': |
|
case 'Select': |
|
case 'Textarea': |
|
case 'SelectFieldLayout': |
|
return 'leafComponent' |
|
|
|
default: |
|
return 'unknownDsComponent' |
|
} |
|
} |
|
|
|
type ChildData = { |
|
child: Element |
|
dsComponentType: ComponentType |
|
isChildOfLeafDsComponent: boolean |
|
rect: Rect |
|
} |
|
type LoopOverChildrenParams<META extends Record<string, unknown>> = { |
|
meta: META |
|
domElement: Element |
|
duration: Duration, |
|
onComplete: (params: { childrenData: ChildData[]; meta: META, duration: Duration, }) => void |
|
|
|
// Must NOT be passed externally |
|
recursiveParams?: { |
|
isRootLoop: boolean // Must not be passed from the consumer |
|
childrenData: ChildData[] // passed recursively |
|
isChildOfLeafDsComponent: boolean |
|
} |
|
} |
|
// Possible optimization: |
|
// - parse the tree depth by depth instead of going through the whole tree at once (since this is hard to split over different frames).The only important thing is that deeper elements are parsed after less deep ones |
|
// - otherwise, I can just use generators |
|
function loopOverDomChildren<META extends Record<string, unknown> = Record<string, unknown>>( |
|
params: LoopOverChildrenParams<META> |
|
) { |
|
const { domElement, meta, duration, onComplete, recursiveParams: { |
|
isRootLoop, |
|
childrenData, |
|
isChildOfLeafDsComponent, |
|
} = { |
|
isRootLoop: true, |
|
childrenData: [], |
|
isChildOfLeafDsComponent: false, |
|
} } = params |
|
|
|
|
|
const childNodes = domElement.children |
|
for (let i = 0, n = childNodes.length; i < n; i++) { |
|
const child = childNodes[i] |
|
if (!child) throw new Error(`No child at ${i} (this should be a TS-only protection)`) |
|
if (child.nodeType !== Node.ELEMENT_NODE) continue |
|
|
|
// Stop when encounter other containers. |
|
// TODO: also add a data-preply-ds-coverage-ignore attribute for external components |
|
const dataPreplyDsCoverageAttribute = child.getAttribute('data-preply-ds-coverage') |
|
if (dataPreplyDsCoverageAttribute !== null) continue |
|
|
|
const isInvisible = globalThis.getComputedStyle(child).display === 'none' |
|
if (isInvisible) continue |
|
|
|
const dsAttribute = child.getAttribute('data-preply-ds-component') |
|
const dsComponentType = getDsComponentType(dsAttribute) |
|
const isLeafDsComponent = dsComponentType === 'leafComponent' |
|
|
|
childrenData.push({ child, isChildOfLeafDsComponent, dsComponentType, rect: fakeRect }) |
|
loopOverDomChildren({ |
|
meta, |
|
onComplete, |
|
duration, |
|
domElement: child, |
|
recursiveParams: { |
|
childrenData, |
|
isRootLoop: false, |
|
isChildOfLeafDsComponent: isLeafDsComponent || isChildOfLeafDsComponent, |
|
} |
|
}) |
|
} |
|
|
|
if (isRootLoop) { |
|
// Will be called only once at the end of the root loop |
|
onComplete({ childrenData, meta, duration }) |
|
} |
|
} |
|
|
|
type AddBoundingRectParams<META extends Record<string, unknown> = Record<string, unknown>> = { |
|
meta: META |
|
duration: Duration |
|
childrenData: ChildData[] |
|
onComplete: (params: { childrenData: ChildData[]; meta: META, duration: Duration }) => void |
|
|
|
// Must NOT be passed externally |
|
recursiveParams?: { |
|
// Ready to be splitted over multiple frames |
|
startAt: number |
|
} |
|
} |
|
function addBoundingRect<META extends Record<string, unknown> = Record<string, unknown>>(params: AddBoundingRectParams<META>) { |
|
const { |
|
meta, |
|
duration, |
|
onComplete, |
|
childrenData, |
|
recursiveParams: { startAt } = { startAt: 0 }, |
|
} = params |
|
|
|
for (let i = startAt, n = childrenData.length; i < n; i++) { |
|
const item = childrenData[i] |
|
if (!item) throw new Error(`No item at ${i} (this should be a TS-only protection)`) |
|
|
|
let rect = item.child.getBoundingClientRect() |
|
item.rect = rect |
|
} |
|
|
|
onComplete({ childrenData, meta, duration }) |
|
} |
|
|
|
type CountPixelsParams<META extends Record<string, unknown>> = { |
|
deadline: IdleDeadline |
|
meta: META |
|
duration: Duration, |
|
childrenData: ChildData[] |
|
viewportPixels: ViewportPixels |
|
offset: { top: number; left: number } |
|
onComplete: (params: { childrenData: ChildData[]; meta: META, pixelCounts: PixelCounts, viewportPixels: ViewportPixels, duration: Duration, offset: { top: number; left: number } }) => void |
|
|
|
// Pass an empty SVG to get the rectangles rendered inside it and visualize the elements rects |
|
svgRenderer?: SVGSVGElement | undefined |
|
|
|
// Must NOT be passed externally |
|
recursiveParams?: { |
|
// Will be splitted over multiple frames |
|
startAt: number |
|
pixelCounts: PixelCounts |
|
} |
|
|
|
} |
|
function countPixels<META extends Record<string, unknown> = Record<string, unknown>>(params: CountPixelsParams<META>) { |
|
|
|
const { |
|
meta, |
|
offset, |
|
deadline, |
|
duration, |
|
onComplete, |
|
svgRenderer, |
|
childrenData, |
|
viewportPixels, |
|
recursiveParams: { startAt, pixelCounts } = { |
|
startAt: 0, |
|
pixelCounts: { |
|
leafComponent: 0, |
|
utilComponent: 0, |
|
nonDsComponent: 0, |
|
layoutComponent: 0, |
|
rebrandComponent: 0, |
|
outdatedComponent: 0, |
|
unknownDsComponent: 0, |
|
}, |
|
} |
|
} = params |
|
|
|
for (let i = startAt, n = childrenData.length, iterations = 0; i < n; i++, iterations++) { |
|
if (deadline.timeRemaining() <= 0) { |
|
log('⏳ Waiting idle') |
|
requestIdleCallback((deadline) => |
|
countPixels({ |
|
meta, |
|
offset, |
|
deadline, |
|
duration, |
|
onComplete, |
|
svgRenderer, |
|
childrenData, |
|
viewportPixels, |
|
recursiveParams: { |
|
startAt: i, |
|
pixelCounts, |
|
}, |
|
}) |
|
) |
|
return |
|
} |
|
|
|
const childData = childrenData[i] |
|
if (!childData) throw new Error(`No childData at ${i} (this should be a TS-only protection)`) |
|
|
|
const { rect, dsComponentType, isChildOfLeafDsComponent } = childData |
|
|
|
const adjustedSsComponentType = isChildOfLeafDsComponent |
|
? 'leafComponent' // children of leaf components are treated as leaf components too |
|
: dsComponentType |
|
|
|
if (svgRenderer) { |
|
// TODO: identify SSR vs browser |
|
const svgRect = globalThis.document.createElementNS('http://www.w3.org/2000/svg', 'rect') |
|
svgRect.setAttribute('x', (rect.left - offset.left).toString()) |
|
svgRect.setAttribute('y', (rect.top - offset.top).toString()) |
|
svgRect.setAttribute('width', rect.width.toString()) |
|
svgRect.setAttribute('height', rect.height.toString()) |
|
svgRect.setAttribute('fill', 'none') |
|
svgRect.setAttribute('stroke-width', '2') |
|
svgRect.setAttribute('opacity', '1') |
|
svgRect.setAttribute('stroke', colorByComponentType[adjustedSsComponentType]) |
|
svgRenderer.appendChild(svgRect) |
|
} |
|
|
|
|
|
const pixelMarker = pixelMarkerByComponentType[adjustedSsComponentType] |
|
|
|
let offsetTop = offset.top |
|
let offsetLeft = offset.left |
|
|
|
let rectTop = rect.top |
|
let rectLeft = rect.left |
|
let rectWidth = rect.width |
|
let rectHeight = rect.height |
|
const firstViewportRow = viewportPixels[0] |
|
if (!firstViewportRow) throw new Error(`No firstViewportRow (this should be a TS-only protection)`) |
|
|
|
const columnLength = firstViewportRow.length |
|
const rowLength = viewportPixels.length |
|
|
|
// debugger |
|
|
|
// "Draw" the rows in viewportPixels, the bidimensional array that repreents the screen |
|
for ( |
|
let firstRow = Math.floor(rectTop - offsetTop), |
|
lastRow = Math.floor(rectTop - offsetTop + rectHeight - 1), |
|
column = Math.floor(rectLeft - offsetLeft); |
|
column < rectLeft - offsetLeft + rectWidth - 1 && column < columnLength; |
|
column++ |
|
) { |
|
if (column < 0) continue // can happen for elements placed outside the viewport |
|
|
|
// "Draw" the top row |
|
if (firstRow >= 0 && firstRow < rowLength && column < columnLength) { |
|
const pixel = viewportPixels[firstRow]?.[column] |
|
if (!!pixel && pixel !== emptyPixel) { |
|
const componentType = componentTypeByPixelMarker[pixel] |
|
pixelCounts[componentType]-- |
|
} |
|
|
|
const pixelRow = viewportPixels[firstRow] |
|
if (pixelRow) { |
|
pixelRow[column] = pixelMarker |
|
pixelCounts[adjustedSsComponentType]++ |
|
} |
|
} |
|
|
|
// "Draw" the bottom row |
|
if (lastRow >= 0 && lastRow < rowLength && column < columnLength) { |
|
const pixel = viewportPixels[lastRow]?.[column] |
|
if (!!pixel && pixel !== emptyPixel) { |
|
const componentType = componentTypeByPixelMarker[pixel] |
|
pixelCounts[componentType]-- |
|
} |
|
|
|
const pixelRow = viewportPixels[lastRow] |
|
if (pixelRow) { |
|
pixelRow[column] = pixelMarker |
|
pixelCounts[adjustedSsComponentType]++ |
|
} |
|
} |
|
} |
|
|
|
// "Draw" the columns in viewportPixels, the bidimensional array that repreents the screen |
|
for ( |
|
let firstColumn = Math.floor(rectLeft - offsetLeft), |
|
lastColumn = Math.floor(rectLeft - offsetLeft + rectWidth - 1), |
|
row = Math.floor(rectTop - offsetTop); |
|
row < rectTop - offsetTop + rectHeight - 1 && row < rowLength; |
|
row++ |
|
) { |
|
if (row < 0) continue // can happen for elements placed outside the viewport |
|
|
|
// "Draw" the left column |
|
if (firstColumn >= 0 && row < rowLength && firstColumn < columnLength) { |
|
const pixel = viewportPixels[row]?.[firstColumn] |
|
if (!!pixel && pixel !== emptyPixel) { |
|
const componentType = componentTypeByPixelMarker[pixel] |
|
pixelCounts[componentType]-- |
|
} |
|
|
|
const pixelRow = viewportPixels[row] |
|
if (pixelRow) { |
|
pixelRow[firstColumn] = pixelMarker |
|
pixelCounts[adjustedSsComponentType]++ |
|
|
|
} |
|
} |
|
|
|
// "Draw" the right row |
|
if (lastColumn >= 0 && row < rowLength && lastColumn < columnLength) { |
|
const pixel = viewportPixels[row]?.[lastColumn] |
|
if (!!pixel && pixel !== emptyPixel) { |
|
const componentType = componentTypeByPixelMarker[pixel] |
|
pixelCounts[componentType]-- |
|
} |
|
|
|
const pixelRow = viewportPixels[row] |
|
if (pixelRow) { |
|
pixelRow[lastColumn] = pixelMarker |
|
pixelCounts[adjustedSsComponentType]++ |
|
} |
|
|
|
} |
|
} |
|
} |
|
|
|
onComplete({ childrenData, pixelCounts, meta, viewportPixels, duration, offset }) |
|
} |
|
|
|
type RunParams<META extends Record<string, unknown>> = { |
|
meta: META |
|
domElement: Element |
|
onComplete: (params: { |
|
meta: META |
|
duration: Duration |
|
pixelCounts: PixelCounts, |
|
childrenData: ChildData[] |
|
viewportPixels: ViewportPixels, |
|
offset: { top: number; left: number } |
|
}) => void |
|
|
|
// Pass an empty SVG to get the rectangles rendered inside it and visualize the elements rects |
|
svgRenderer?: SVGSVGElement | undefined |
|
} |
|
function run<META extends Record<string, unknown> = Record<string, unknown>>({ |
|
meta, |
|
domElement, |
|
onComplete, |
|
svgRenderer, |
|
}: RunParams<META>) { |
|
const start = Date.now() // duration.now is more precise but takes more time to be executed |
|
let step = start |
|
|
|
let countPixelsTime = 0 |
|
let addBoundingRectTime = 0 |
|
let loopOverDomChildrenTime = 0 |
|
|
|
requestIdleCallback(() => { |
|
loopOverDomChildren({ |
|
meta, |
|
domElement, |
|
duration: { |
|
totalTime: -1, |
|
countPixelsTime: -1, |
|
addBoundingRectTime: -1, |
|
loopOverDomChildrenTime: -1, |
|
}, |
|
onComplete: ({ childrenData, meta, duration }) => { |
|
log('✅ loopOverDomChildren') |
|
const stepBefore = step |
|
step = Date.now() // duration.now is more precise but takes more time to be executed |
|
loopOverDomChildrenTime = step - stepBefore |
|
|
|
addBoundingRect({ |
|
childrenData, |
|
meta, |
|
duration: { ...duration, loopOverDomChildrenTime }, |
|
onComplete: ({ childrenData, meta, duration }) => { |
|
log('✅ addBoundingRect') |
|
|
|
const stepBefore = step |
|
step = Date.now() // duration.now is more precise but takes more time to be executed |
|
addBoundingRectTime = step - stepBefore |
|
|
|
const elementRect = domElement.getBoundingClientRect() |
|
|
|
// Helpful to unit test countPixels |
|
// const elementRect = { top: 0, left: 0, width: 10, height: 10 } |
|
// childrenData = [ |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'nonDsComponent', |
|
// rect: { top: 0, left: 0, width: 10, height: 10 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'layoutComponent', |
|
// rect: { top: 1, left: 1, width: 8, height: 8 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'utilComponent', |
|
// rect: { top: 2, left: 2, width: 6, height: 6 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'outdatedComponent', |
|
// rect: { top: 3, left: 3, width: 4, height: 4 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'leafComponent', |
|
// rect: { top: 4, left: 4, width: 2, height: 2 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'unknownDsComponent', |
|
// rect: { top: 1, left: 1, width: 8, height: 8 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// { |
|
// isChildOfLeafDsComponent: false, |
|
// dsComponentType: 'rebrandComponent', |
|
// rect: { top: 1, left: 1, width: 8, height: 8 }, |
|
// // @ts-ignore |
|
// child: undefined |
|
// }, |
|
// ] |
|
|
|
const elementWidth = Math.floor(elementRect.width) |
|
const elementHeight = Math.floor(elementRect.height) |
|
|
|
/** |
|
* Think of it as a data mirror of the screen: every pixel of the screen has value in the bidimensional array. It starts empty but it will look like this at the end |
|
* [ |
|
* [ ,_,_,_,_,_,_,_,_,_,], |
|
* [ ,_, , , , , , , ,_,], |
|
* [ ,_,_,_,_,_,_,_,_,_,], |
|
* [ , , , , , , , , , ,], |
|
* [ , , , , ,L,L,L,L,L,], |
|
* [ , , , , ,L, , , ,L,], |
|
* [ , , , , ,L,L,L,L,L,], |
|
* ] |
|
* where every letter perimeter describes an element perimeter. |
|
* (a real one could be 800 like the real screen) |
|
*/ |
|
const viewportPixels: ViewportPixels = new Array(elementHeight).fill([]).map(() => |
|
new Array(elementWidth).fill(emptyPixel) |
|
) |
|
const offset = { left: elementRect.left, top: elementRect.top } |
|
|
|
requestIdleCallback((deadline) => { |
|
countPixels({ |
|
meta, |
|
offset, |
|
deadline, |
|
svgRenderer, |
|
childrenData, |
|
viewportPixels, |
|
duration: { ...duration, addBoundingRectTime }, |
|
onComplete: ({ meta, pixelCounts, childrenData, viewportPixels, duration, offset }) => { |
|
log('✅ countPixels') |
|
|
|
const stepBefore = step |
|
step = Date.now() // duration.now is more precise but takes more time to be executed |
|
countPixelsTime = step - stepBefore |
|
|
|
const totalTime = Date.now() - start // duration.now is more precise but takes more time to be executed - start |
|
|
|
|
|
onComplete({ |
|
childrenData, |
|
viewportPixels, |
|
pixelCounts, |
|
meta, |
|
offset, |
|
duration: { ...duration, totalTime, countPixelsTime }, |
|
}) |
|
}, |
|
}) |
|
}) |
|
}, |
|
}) |
|
}, |
|
}) |
|
}) |
|
|
|
} |
|
|
|
function shouldRun() { |
|
const now = Date.now() |
|
|
|
// Convert the time to minutes |
|
const totalMinutes = Math.floor(now / 1000 / 60) |
|
|
|
// Get the current minute of the hour |
|
const currentMinute = totalMinutes % 60 |
|
|
|
// By default, let's run the coverage at the end of the hour, when the previous lesson is over and the next lesson is about to start |
|
const defaultMinutes = 1 |
|
const runDuringLastHourMinutes = |
|
// Allow setting the minutes externally |
|
// @ts-expect-error the dsCoverageLastHourMinutes global variable sould be typed |
|
globalThis.dsCoverageLastHourMinutes ?? defaultMinutes |
|
|
|
return currentMinute > 60 - runDuringLastHourMinutes |
|
} |
|
|
|
function queueDsCoverage() { |
|
function tick() { |
|
// Every tot milliseconds, let's check if we can run the coverage |
|
if (!('requestIdleCallback' in globalThis)) { |
|
// Safari does not support requestIdleCallback |
|
clearInterval(coverageIntervalId) |
|
log('No requestIdleCallback') |
|
return |
|
} |
|
|
|
const forceCalculation = true |
|
|
|
// TODO: identify SSR vs browser |
|
const isSupposedToBePowerful = globalThis.document.documentElement.clientWidth >= 1280 |
|
|
|
// The initial implementation runs loopOverDomChildren and addBoundingRect without interruptions. |
|
// The advantage is that we get the elements immediately and we canculate the coverage later on |
|
if (!isSupposedToBePowerful && !forceCalculation) { |
|
log('Machine is not supposed to be powerful') |
|
return |
|
} |
|
|
|
// Limit when we run it |
|
if (!shouldRun()) { |
|
log('SHould not run now') |
|
return |
|
} |
|
|
|
// data-preply-ds-coverage must be unique in page |
|
// TODO: identify SSR vs browser |
|
const elementsToAnalyze = globalThis.document.querySelectorAll('[data-preply-ds-coverage]') |
|
log(`Found ${elementsToAnalyze.length} DS coverage containers`) |
|
if (elementsToAnalyze.length === 0) return |
|
|
|
// No need to re-run the calculation in standard MPA apps like node-ssr (will be needed for SPAs like Preply Space, though) |
|
clearInterval(coverageIntervalId) |
|
|
|
log(`🎬 Calculation start`) |
|
|
|
const results: Results = {} |
|
for (let i = 0, n = elementsToAnalyze.length; i < n; i++) { |
|
const domElement = elementsToAnalyze[i] |
|
|
|
if (!domElement) throw new Error(`No element at ${i} (this should be a TS-only protection)`) |
|
|
|
// By passing an svg, the element's rect will be added there and we can visualize them by adding the svg to the page |
|
const svgRenderer = undefined |
|
// let svgRenderer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') |
|
// svgRenderer.setAttribute('width', width) |
|
// svgRenderer.setAttribute('height', height) |
|
// svgRenderer.style.position = 'fixed' |
|
// svgRenderer.style.zIndex = 1000 |
|
// Set svg top and left to the received offset |
|
// element.appendChild(svgRenderer) |
|
|
|
const coverageAttribute = domElement?.getAttribute('data-preply-ds-coverage') |
|
if (!coverageAttribute) throw new Error(`No element or attribute (this should be a TS-only protection)`) |
|
|
|
results[coverageAttribute] = { done: false } |
|
run({ |
|
domElement, |
|
svgRenderer, |
|
meta: { |
|
dsCoverageVersion: '1', // should be aligned to the DS version |
|
coverageAttribute, |
|
href: globalThis.location.href, |
|
}, |
|
onComplete: ({ |
|
meta, |
|
duration, |
|
pixelCounts, |
|
childrenData, |
|
viewportPixels, |
|
offset |
|
}) => { |
|
const coverageAttribute = meta.coverageAttribute |
|
const result: Result = { |
|
done: true, |
|
duration, |
|
pixelCounts, |
|
viewportPixels, |
|
href: meta.href, |
|
dsCoverageVersion: meta.dsCoverageVersion, |
|
} |
|
results[coverageAttribute] = result |
|
|
|
if (Object.values(results).some((value) => !value.done)) return |
|
|
|
log(`🏁 Calculation end`) |
|
console.table(duration) |
|
|
|
for (const [key, result] of Object.entries(results)) { |
|
if (!result.done) return |
|
const { |
|
pixelCounts, |
|
viewportPixels |
|
} = result |
|
|
|
console.group(key); |
|
const dsPixels = pixelCounts.layoutComponent + |
|
pixelCounts.utilComponent + |
|
pixelCounts.outdatedComponent + |
|
pixelCounts.leafComponent + |
|
pixelCounts.unknownDsComponent |
|
const nonDsPixels = pixelCounts.nonDsComponent + pixelCounts.rebrandComponent |
|
log(`Coverage: ${((dsPixels / (nonDsPixels + dsPixels)) * 100).toFixed(2)} %`) |
|
console.table(pixelCounts) |
|
|
|
// Transform the array in a string and print it |
|
// let string = '' |
|
// viewportPixels.forEach((row) => { |
|
// string += row.join('') + '\n' |
|
// }) |
|
// console.log(string) |
|
|
|
console.groupEnd(); |
|
|
|
|
|
} |
|
}, |
|
}) |
|
} |
|
} |
|
|
|
const coverageIntervalId = setInterval(tick, 1000) |
|
} |
|
|
|
// Temp hack |
|
// TODO: identify SSR vs browser |
|
globalThis.document.body.setAttribute('data-preply-ds-coverage', '{TODO:1}') |
|
// document |
|
// .querySelectorAll('[data-preply-ds-component="RebrandStackedImage"]')[0] |
|
// .setAttribute('data-preply-ds-coverage', '{TODO:2}') |
|
|
|
queueDsCoverage() |
|
// @ts-expect-error the dsCoverageLastHourMinutes global variable sould be typed |
|
globalThis.dsCoverageLastHourMinutes = 60 |
|
|
|
function log(...args: unknown[]) { |
|
console.log( |
|
'%c Preply DS coverage ', |
|
// color.background.brand, color.text.primary |
|
'background: #FF7AAC; color: #121117; padding: 2px; border-radius: 2px;', |
|
...args |
|
); |
|
} |
|
})() |