Skip to content

Commit 116434a

Browse files
committed
Track Responsive-Design carousel resize events
1 parent 0f34e57 commit 116434a

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

experimental/tests.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { getTodoText } from "../resources/shared/translations.mjs";
33
import { numberOfItemsToAdd } from "../resources/shared/todomvc-utils.mjs";
44
import { freezeSuites } from "../resources/suites-helper.mjs";
55

6+
function observeRecipeCarouselResizeObservations(page) {
7+
return page.querySelector(".carousel", ["cooking-app", "main-content", "recipe-carousel"]).observeResizeEvents();
8+
}
9+
10+
function reportRecipeCarouselResizeObservations(stepName, resizeObservations) {
11+
const { count } = resizeObservations;
12+
resizeObservations.disconnect();
13+
if (count)
14+
console.warn(`${stepName}: recipe-carousel ResizeObserver observed ${count} distinct width change(s).`);
15+
else
16+
console.warn(`${stepName}: recipe-carousel ResizeObserver observed 0 distinct width changes; expected width changes during iframe resize.`);
17+
}
18+
619
export const ExperimentalSuites = freezeSuites([
720
{
821
name: "TodoMVC-LocalStorage",
@@ -193,6 +206,9 @@ export const ExperimentalSuites = freezeSuites([
193206
new BenchmarkTestStep("ReduceWidthIn5Steps", async (page) => {
194207
const widths = [768, 704, 640, 560, 480];
195208
const MATCH_MEDIA_QUERY_BREAKPOINT = 640;
209+
const carouselResizeObservations = observeRecipeCarouselResizeObservations(page);
210+
// Seed the baseline width before the synchronous width changes below.
211+
await carouselResizeObservations.ready;
196212

197213
// The matchMedia query is "(max-width: 640px)"
198214
// Starting from a width > 640px, we'll only get 1 event when crossing to <= 640px
@@ -209,6 +225,7 @@ export const ExperimentalSuites = freezeSuites([
209225
}
210226

211227
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
228+
reportRecipeCarouselResizeObservations("ReduceWidthIn5Steps", carouselResizeObservations);
212229
}),
213230
new BenchmarkTestStep("ScrollToChatAndSendMessages", async (page) => {
214231
const cvWorkComplete = new Promise((resolve) => {
@@ -252,6 +269,9 @@ export const ExperimentalSuites = freezeSuites([
252269
new BenchmarkTestStep("IncreaseWidthIn5Steps", async (page) => {
253270
const widths = [560, 640, 704, 768, 800];
254271
const MATCH_MEDIA_QUERY_BREAKPOINT = 704;
272+
const carouselResizeObservations = observeRecipeCarouselResizeObservations(page);
273+
// Seed the baseline width before the synchronous width changes below.
274+
await carouselResizeObservations.ready;
255275

256276
// The matchMedia query is "(max-width: 640px)"
257277
// Starting from a width <= 640px, we'll get 1 event when crossing back to > 640px.
@@ -268,6 +288,7 @@ export const ExperimentalSuites = freezeSuites([
268288
}
269289

270290
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
291+
reportRecipeCarouselResizeObservations("IncreaseWidthIn5Steps", carouselResizeObservations);
271292
}),
272293
],
273294
},

resources/benchmark-runner.mjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,46 @@ class PageElement {
163163
this.#node.scrollIntoView(options);
164164
}
165165

166+
observeResizeEvents() {
167+
const contentWindow = this.#node.ownerDocument.defaultView;
168+
const state = {
169+
count: 0,
170+
lastWidth: null,
171+
};
172+
let markReady;
173+
// Resolves on the first delivery so callers can seed a baseline before resizing.
174+
const ready = new Promise((resolve) => {
175+
markReady = resolve;
176+
});
177+
const observer = new contentWindow.ResizeObserver((entries) => {
178+
for (const entry of entries) {
179+
const contentBoxSize = entry.contentBoxSize;
180+
const inlineSize = Array.isArray(contentBoxSize) ? contentBoxSize[0]?.inlineSize : contentBoxSize?.inlineSize;
181+
const width = inlineSize ?? entry.contentRect.width;
182+
// The first delivery seeds the baseline width without counting it as a change.
183+
if (state.lastWidth === null) {
184+
state.lastWidth = width;
185+
markReady();
186+
continue;
187+
}
188+
if (width === state.lastWidth)
189+
continue;
190+
state.count++;
191+
state.lastWidth = width;
192+
}
193+
});
194+
observer.observe(this.#node);
195+
return {
196+
ready,
197+
get count() {
198+
return state.count;
199+
},
200+
disconnect() {
201+
observer.disconnect();
202+
},
203+
};
204+
}
205+
166206
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
167207
if (eventName === "submit")
168208
// FIXME FireFox doesn't like `new Event('submit')

tests/unittests/benchmark-runner.mjs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,137 @@ describe("BenchmarkRunner", () => {
257257
});
258258
});
259259
});
260+
261+
describe("PageElement", () => {
262+
async function withPageElement(configureFrame, callback) {
263+
let fixtureNode;
264+
let result;
265+
const runner = new BenchmarkRunner(
266+
[
267+
{
268+
name: "PageElement fixture",
269+
enabled: true,
270+
url: "about:blank",
271+
async prepare(page) {
272+
result = await callback(page.getElementById("resize-target"), fixtureNode);
273+
},
274+
tests: [],
275+
},
276+
],
277+
{}
278+
);
279+
sinon.stub(SuiteRunner.prototype, "_loadFrame").callsFake(async function () {
280+
fixtureNode = configureFrame(this.frame);
281+
});
282+
sinon.stub(SuiteRunner.prototype, "_runSuite").callsFake(async () => {});
283+
await runner.runAllSuites();
284+
return result;
285+
}
286+
287+
describe("observeResizeEvents", () => {
288+
// A fake ResizeObserver drives deliveries synchronously for exact-count assertions.
289+
async function createTracker() {
290+
let deliver;
291+
class FakeResizeObserver {
292+
constructor(callback) {
293+
deliver = (...widths) => callback(widths.map((width) => ({ contentBoxSize: [{ inlineSize: width }] })));
294+
}
295+
observe() {}
296+
disconnect() {}
297+
}
298+
return withPageElement(
299+
(frame) => {
300+
Object.defineProperty(frame.contentWindow, "ResizeObserver", { configurable: true, value: FakeResizeObserver });
301+
const node = frame.contentDocument.createElement("div");
302+
node.id = "resize-target";
303+
frame.contentDocument.body.appendChild(node);
304+
return node;
305+
},
306+
(element) => ({
307+
resizeEvents: element.observeResizeEvents(),
308+
deliver: (...widths) => deliver(...widths),
309+
})
310+
);
311+
}
312+
313+
it("treats the first delivery as the baseline without counting it", async () => {
314+
const { resizeEvents, deliver } = await createTracker();
315+
deliver(200);
316+
await resizeEvents.ready;
317+
expect(resizeEvents.count).to.equal(0);
318+
});
319+
320+
it("counts each distinct width change exactly once", async () => {
321+
const { resizeEvents, deliver } = await createTracker();
322+
deliver(200); // seed
323+
deliver(300);
324+
deliver(400);
325+
deliver(500);
326+
expect(resizeEvents.count).to.equal(3);
327+
});
328+
329+
it("does not count deliveries that report the same width", async () => {
330+
const { resizeEvents, deliver } = await createTracker();
331+
deliver(200); // seed
332+
deliver(300);
333+
deliver(300);
334+
deliver(300);
335+
expect(resizeEvents.count).to.equal(1);
336+
});
337+
});
338+
describe("observeResizeEvents with a real ResizeObserver", () => {
339+
// border-box width minus 20px padding and 10px border on each side.
340+
const BORDER_BOX_INSET = 60;
341+
342+
function appendBox(document, borderBoxWidth) {
343+
const div = document.createElement("div");
344+
div.id = "resize-target";
345+
div.style.cssText = `box-sizing: border-box; padding: 20px; border: 10px solid black; position: absolute; top: -9999px; left: -9999px; height: 50px; width: ${borderBoxWidth}px;`;
346+
document.body.appendChild(div);
347+
return div;
348+
}
349+
350+
// Resolves once a probe observer sees the expected content-box width. The
351+
// tracker is created first, so it is notified first in the same frame.
352+
function waitForContentWidth(element, expectedWidth, timeoutMs = 2000) {
353+
return new Promise((resolve, reject) => {
354+
let observer;
355+
const timeout = setTimeout(() => {
356+
observer.disconnect();
357+
reject(new Error(`Timed out waiting for content-box width ${expectedWidth}px`));
358+
}, timeoutMs);
359+
const ResizeObserver = element.ownerDocument.defaultView.ResizeObserver;
360+
observer = new ResizeObserver((entries) => {
361+
for (const entry of entries) {
362+
const contentBoxSize = entry.contentBoxSize;
363+
const inlineSize = Array.isArray(contentBoxSize) ? contentBoxSize[0]?.inlineSize : contentBoxSize?.inlineSize;
364+
const width = inlineSize ?? entry.contentRect.width;
365+
if (width === expectedWidth) {
366+
clearTimeout(timeout);
367+
observer.disconnect();
368+
resolve();
369+
return;
370+
}
371+
}
372+
});
373+
observer.observe(element);
374+
});
375+
}
376+
377+
it("counts each distinct width change exactly once", async () => {
378+
await withPageElement(
379+
(frame) => appendBox(frame.contentDocument, 200),
380+
async (element, div) => {
381+
const resizeEvents = element.observeResizeEvents();
382+
await resizeEvents.ready;
383+
div.style.width = "300px";
384+
await waitForContentWidth(div, 300 - BORDER_BOX_INSET);
385+
div.style.width = "400px";
386+
await waitForContentWidth(div, 400 - BORDER_BOX_INSET);
387+
expect(resizeEvents.count).to.equal(2);
388+
resizeEvents.disconnect();
389+
}
390+
);
391+
});
392+
});
393+
});

0 commit comments

Comments
 (0)