1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
// Utilities for Layout Instability tests.
// Returns a promise that is resolved when the specified number of animation
// frames has occurred.
waitForAnimationFrames = frameCount => {
return new Promise(resolve => {
const handleFrame = () => {
if (--frameCount <= 0)
resolve();
else
requestAnimationFrame(handleFrame);
};
requestAnimationFrame(handleFrame);
});
};
// Returns a promise that is resolved when the next animation frame occurs.
waitForAnimationFrame = () => waitForAnimationFrames(1);
// Helper to compute an expected layout shift score based on an expected impact
// region and max move distance for a particular animation frame.
computeExpectedScore = (impactRegionArea, moveDistance) => {
const docElement = document.documentElement;
const viewWidth = docElement.clientWidth;
const viewHeight = docElement.clientHeight;
const viewArea = viewWidth * viewHeight;
const viewMaxDim = Math.max(viewWidth, viewHeight);
const impactFraction = impactRegionArea / viewArea;
const distanceFraction = moveDistance / viewMaxDim;
return impactFraction * distanceFraction;
};
// An list to record all the entries with startTime and score.
let watcher_entry_record = [];
// An object that tracks the document cumulative layout shift score.
// Usage:
//
// const watcher = new ScoreWatcher;
// ...
// assert_equals(watcher.score, expectedScore);
//
// The score reflects only layout shifts that occur after the ScoreWatcher is
// constructed.
ScoreWatcher = function() {
if (PerformanceObserver.supportedEntryTypes.indexOf("layout-shift") == -1)
throw new Error("Layout Instability API not supported");
this.score = 0;
this.scoreWithInputExclusion = 0;
const resetPromise = () => {
this.promise = new Promise(resolve => {
this.resolve = () => {
resetPromise();
resolve();
}
});
};
resetPromise();
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
this.lastEntry = entry;
this.score += entry.value;
watcher_entry_record.push({startTime: entry.startTime, score: entry.value, hadRecentInput : entry.hadRecentInput});
if (!entry.hadRecentInput)
this.scoreWithInputExclusion += entry.value;
this.resolve();
});
});
observer.observe({entryTypes: ['layout-shift']});
};
ScoreWatcher.prototype.checkExpectation = function(expectation) {
if (expectation.score != undefined)
assert_equals(this.score, expectation.score);
if (expectation.sources)
check_sources(expectation.sources, this.lastEntry.sources);
};
ScoreWatcher.prototype.get_entry_record = function() {
return watcher_entry_record;
};
check_sources = (expect_sources, actual_sources) => {
assert_equals(expect_sources.length, actual_sources.length);
let rect_match = (e, a) =>
e[0] == a.x && e[1] == a.y && e[2] == a.width && e[3] == a.height;
let match = e => a =>
e.node === a.node &&
rect_match(e.previousRect, a.previousRect) &&
rect_match(e.currentRect, a.currentRect);
for (let e of expect_sources)
assert_true(actual_sources.some(match(e)), e.node + " not found");
};
|