zerospikes

Writing scenarios

A scenario is one zs.measure call: the interaction in act, run several times. Every run must start from the same state, so put the page back with before or after, which aren't measured.

Typing

const search = page.getByRole("searchbox");
await zs.measure("type 'mar'", () => search.pressSequentially("mar"), {
  before: () => search.fill(""),
});

Use pressSequentially: fill sets the value in one step and produces no key events.

Clicking

await zs.measure("click the header", () => page.getByRole("button", { name: /^Client/ }).click());

Dragging

await zs.measure("drag the border", () => zs.drag([x, y], [x + 160, y]), {
  after: () => zs.drag([x + 160, y], [x, y], { steps: 2 }), // put it back
});

zs.drag sends one pointermove per frame, so slow frames slow the gesture down like they would for a person.

Scrolling

await zs.measure(
  "15 wheel steps",
  async () => {
    for (let i = 0; i < 15; i++) {
      await page.mouse.wheel(0, 400);
      await zs.frame(); // a frame between events
    }
  },
  { before: () => body.evaluate((el) => void (el.scrollTop = 0)) },
);

Memory leaks

await zs.cycles("open and close 10 invoices", async () => {
  for (let i = 0; i < 10; i++) {
    await page.locator(".grid-row").nth(i).click();
    await page.getByRole("button", { name: "Close detail" }).click();
  }
});

The cycle must leave the page as it found it. A leak shows as growth in memory.nodes and memory.listeners. Chromium only.

Guidelines

  • One interaction per scenario: one that does five things hides which one regressed.
  • Measure a production build, with workers: 1.
  • Turn off overlays that animate on their own, such as live performance meters.
  • Keep names stable: they identify the scenario's limits.