summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
blob: 3ac3705e8e7ad2403715a0983445a567029dd044 (plain)
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const image_delay = 1000;
const delay_pipe_value = image_delay / 1000;

const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => {
  let timeout_id;
  const timeout = new Promise((_, reject) => {
    timeout_id = step_timeout(() =>
      reject(new DOMException(message, "TimeoutError")), delay)
  });
  let result = null;
  try {
    result = await Promise.race([promise, timeout]);
    clearTimeout(timeout_id);
  } finally {
    cleanup();
  }
  return result;
};

// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
// The |options| parameter may contain some string values specifying the following:
// * 'renderTimeIs0': the renderTime should be 0 (image does not pass Timing-Allow-Origin checks).
//     When not present, the renderTime should not be 0 (image passes the checks).
// * 'sizeLowerBound': the |expectedSize| is only a lower bound on the size attribute value.
//     When not present, |expectedSize| must be exactly equal to the size attribute value.
// * 'approximateSize': the |expectedSize| is only approximate to the size attribute value.
//     This option is mutually exclusive to 'sizeLowerBound'.
function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound, options = []) {
  assert_equals(entry.name, '', "Entry name should be the empty string");
  assert_equals(entry.entryType, 'largest-contentful-paint',
    "Entry type should be largest-contentful-paint");
  assert_equals(entry.duration, 0, "Entry duration should be 0");
  // The entry's url can be truncated.
  assert_equals(expectedUrl.substr(0, 100), entry.url.substr(0, 100),
    `Expected URL ${expectedUrl} should at least start with the entry's URL ${entry.url}`);
  assert_equals(entry.id, expectedID, "Entry ID matches expected one");
  assert_equals(entry.element, document.getElementById(expectedID),
    "Entry element is expected one");
  if (options.includes('skip')) {
    return;
  }
  if (options.includes('renderTimeIs0')) {
    assert_equals(entry.renderTime, 0, 'renderTime should be 0');
    assert_between_exclusive(entry.loadTime, timeLowerBound, performance.now(),
      'loadTime should be between the lower bound and the current time');
    assert_approx_equals(entry.startTime, entry.loadTime, 0.001,
      'startTime should be equal to renderTime to the precision of 1 millisecond.');
  } else {
    assert_between_exclusive(entry.loadTime, timeLowerBound, entry.renderTime,
      'loadTime should occur between the lower bound and the renderTime');
    assert_greater_than_equal(performance.now(), entry.renderTime,
      'renderTime should occur before the entry is dispatched to the observer.');
    assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
      'startTime should be equal to renderTime to the precision of 1 millisecond.');
  }
  if (options.includes('sizeLowerBound')) {
    assert_greater_than(entry.size, expectedSize);
  } else if (options.includes('approximateSize')) {
    assert_approx_equals(entry.size, expectedSize, 1);
  } else{
    assert_equals(entry.size, expectedSize);
  }

  if (options.includes('animated')) {
    assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
      'firstAnimatedFrameTime should be smaller than loadTime');
    assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
      'firstAnimatedFrameTime should be smaller than renderTime');
    assert_less_than(entry.firstAnimatedFrameTime, image_delay,
      'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
    assert_greater_than(entry.firstAnimatedFrameTime, 0,
      'firstAnimatedFrameTime should be larger than 0');
  }
  if (options.includes('animated-zero')) {
    assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
  }
}

const load_and_observe = url => {
  return new Promise(resolve => {
    (new PerformanceObserver(entryList => {
      for (let entry of entryList.getEntries()) {
        if (entry.url == url) {
          resolve(entryList.getEntries()[0]);
        }
      }
    })).observe({ type: 'largest-contentful-paint', buffered: true });
    const img = new Image();
    img.id = 'image_id';
    img.src = url;
    document.body.appendChild(img);
  });
};

const load_video_and_observe = url => {
  return new Promise(resolve => {
    (new PerformanceObserver(entryList => {
      for (let entry of entryList.getEntries()) {
        if (entry.url == url) {
          resolve(entryList.getEntries()[0]);
        }
      }
    })).observe({ type: 'largest-contentful-paint', buffered: true });
    const video = document.createElement("video");
    video.id = 'video_id';
    video.src = url;
    video.autoplay = true;
    video.muted = true;
    video.loop = true;
    document.body.appendChild(video);
  });
};

const getLCPStartTime = (identifier) => {
  return new Promise(resolve => {
    new PerformanceObserver((entryList, observer) => {
      entryList.getEntries().forEach(e => {
        if (e.url.includes(identifier)) {
          resolve(e);
          observer.disconnect();
        }
      });
    }).observe({ type: 'largest-contentful-paint', buffered: true });
  });
}

const getFCPStartTime = () => {
  return performance.getEntriesByName('first-contentful-paint')[0];
}

const add_text = (text) => {
  const paragraph = document.createElement('p');
  paragraph.innerHTML = text;
  document.body.appendChild(paragraph);
}

const loadImage = (url, shouldBeIgnoredForLCP = false) => {
  return new Promise(function (resolve, reject) {
    let image = document.createElement('img');
    image.addEventListener('load', () => { resolve(image); });
    image.addEventListener('error', reject);
    image.src = url;
    if (shouldBeIgnoredForLCP)
      image.style.opacity = 0;
    document.body.appendChild(image);
  });
}

const checkLCPEntryForNonTaoImages = (times = {}) => {
  const lcp = times['lcp'];
  const fcp = times['fcp'];
  const lcp_url_components = lcp.url.split('/');

  if (lcp.loadTime <= fcp.startTime) {
    assert_approx_equals(lcp.startTime, fcp.startTime, 0.001,
      'LCP start time should be the same as FCP for ' +
      lcp_url_components[lcp_url_components.length - 1]) +
      ' when LCP load time is less than FCP.';
  } else {
    assert_approx_equals(lcp.startTime, lcp.loadTime, 0.001,
      'LCP start time should be the same as LCP load time for ' +
      lcp_url_components[lcp_url_components.length - 1]) +
      ' when LCP load time is no less than FCP.';
  }

  assert_equals(lcp.renderTime, 0,
    'The LCP render time of Non-Tao image should always be 0.');
}

const raf = () => {
  return new Promise(resolve => requestAnimationFrame(resolve));
}