Skip to content

Commit 9304efe

Browse files
authored
Track Responsive-Design carousel resize events (#579)
* Track Responsive-Design carousel resize events
1 parent 67117c1 commit 9304efe

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

resources/benchmark-runner.mjs

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

166+
async 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 callback so the initial width can be used as a baseline.
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 callback 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+
await ready;
196+
return {
197+
stop() {
198+
observer.disconnect();
199+
return state.count;
200+
},
201+
};
202+
}
203+
166204
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
167205
if (eventName === "submit")
168206
// FIXME FireFox doesn't like `new Event('submit')

suites-experimental/suites.mjs

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

6+
function reportRecipeCarouselResizeEvents(stepName, resizeEvents) {
7+
const count = resizeEvents.stop();
8+
if (count)
9+
console.warn(`${stepName}: recipe-carousel ResizeObserver reported ${count} width change(s).`);
10+
else
11+
console.warn(`${stepName}: recipe-carousel ResizeObserver reported 0 width changes; expected width changes during iframe resize.`);
12+
}
13+
614
export const ExperimentalSuites = freezeSuites([
715
{
816
name: "TodoMVC-LocalStorage",
@@ -214,6 +222,7 @@ export const ExperimentalSuites = freezeSuites([
214222
new BenchmarkTestStep("ReduceWidthIn5Steps", async (page) => {
215223
const widths = [768, 704, 640, 560, 480];
216224
const MATCH_MEDIA_QUERY_BREAKPOINT = 640;
225+
const carouselResizeEvents = await page.querySelector(".carousel", ["cooking-app", "main-content", "recipe-carousel"]).observeResizeEvents();
217226

218227
// The matchMedia query is "(max-width: 640px)"
219228
// Starting from a width > 640px, we'll only get 1 event when crossing to <= 640px
@@ -230,6 +239,7 @@ export const ExperimentalSuites = freezeSuites([
230239
}
231240

232241
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
242+
reportRecipeCarouselResizeEvents("ReduceWidthIn5Steps", carouselResizeEvents);
233243
}),
234244
new BenchmarkTestStep("ScrollToChatAndSendMessages", async (page) => {
235245
const cvWorkComplete = new Promise((resolve) => {
@@ -273,6 +283,7 @@ export const ExperimentalSuites = freezeSuites([
273283
new BenchmarkTestStep("IncreaseWidthIn5Steps", async (page) => {
274284
const widths = [560, 640, 704, 768, 800];
275285
const MATCH_MEDIA_QUERY_BREAKPOINT = 704;
286+
const carouselResizeEvents = await page.querySelector(".carousel", ["cooking-app", "main-content", "recipe-carousel"]).observeResizeEvents();
276287

277288
// The matchMedia query is "(max-width: 640px)"
278289
// Starting from a width <= 640px, we'll get 1 event when crossing back to > 640px.
@@ -289,6 +300,7 @@ export const ExperimentalSuites = freezeSuites([
289300
}
290301

291302
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
303+
reportRecipeCarouselResizeEvents("IncreaseWidthIn5Steps", carouselResizeEvents);
292304
}),
293305
],
294306
},

tests/unittests/benchmark-runner.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,93 @@ describe("BenchmarkRunner", () => {
307307
});
308308
});
309309
});
310+
311+
describe("PageElement", () => {
312+
describe("observeResizeEvents", () => {
313+
before(function () {
314+
skipInShell(this);
315+
});
316+
317+
async function withPageElement(configureFrame, callback) {
318+
let fixtureNode;
319+
let result;
320+
const runner = new BenchmarkRunner(
321+
[
322+
{
323+
name: "PageElement fixture",
324+
enabled: true,
325+
url: "about:blank",
326+
async prepare(page) {
327+
result = await callback(page.getElementById("resize-target"), fixtureNode);
328+
},
329+
tests: [],
330+
},
331+
],
332+
{}
333+
);
334+
sinon.stub(SuiteRunner.prototype, "_loadFrame").callsFake(async function () {
335+
fixtureNode = configureFrame(this.frame);
336+
});
337+
sinon.stub(SuiteRunner.prototype, "_runSuite").callsFake(async () => {});
338+
await runner.runAllSuites();
339+
return result;
340+
}
341+
342+
// A fake ResizeObserver drives callbacks synchronously for exact-count assertions.
343+
async function createTracker() {
344+
let deliver;
345+
const disconnect = sinon.stub();
346+
class FakeResizeObserver {
347+
constructor(callback) {
348+
deliver = (...widths) => callback(widths.map((width) => ({ contentBoxSize: [{ inlineSize: width }] })));
349+
}
350+
observe() {}
351+
disconnect() {
352+
disconnect();
353+
}
354+
}
355+
return withPageElement(
356+
(frame) => {
357+
Object.defineProperty(frame.contentWindow, "ResizeObserver", { configurable: true, value: FakeResizeObserver });
358+
const node = frame.contentDocument.createElement("div");
359+
node.id = "resize-target";
360+
frame.contentDocument.body.appendChild(node);
361+
return node;
362+
},
363+
(element) => ({
364+
resizeEventsPromise: element.observeResizeEvents(),
365+
deliver: (...widths) => deliver(...widths),
366+
disconnect,
367+
})
368+
);
369+
}
370+
371+
it("treats the first callback as the baseline without counting it", async () => {
372+
const { resizeEventsPromise, deliver, disconnect } = await createTracker();
373+
deliver(200);
374+
const resizeEvents = await resizeEventsPromise;
375+
expect(resizeEvents.stop()).to.equal(0);
376+
sinon.assert.calledOnce(disconnect);
377+
});
378+
379+
it("counts each distinct width change exactly once", async () => {
380+
const { resizeEventsPromise, deliver } = await createTracker();
381+
deliver(200); // seed
382+
const resizeEvents = await resizeEventsPromise;
383+
deliver(300);
384+
deliver(400);
385+
deliver(500);
386+
expect(resizeEvents.stop()).to.equal(3);
387+
});
388+
389+
it("does not count callbacks that report the same width", async () => {
390+
const { resizeEventsPromise, deliver } = await createTracker();
391+
deliver(200); // seed
392+
const resizeEvents = await resizeEventsPromise;
393+
deliver(300);
394+
deliver(300);
395+
deliver(300);
396+
expect(resizeEvents.stop()).to.equal(1);
397+
});
398+
});
399+
});

0 commit comments

Comments
 (0)