Skip to content

Hooks: ctx.topLevel runs the top-level hook a route's own replaces - #117

Merged
tmashara merged 1 commit into
mainfrom
claude/toplevel-hook
Nov 2, 2026
Merged

tmashara merged 1 commit into
mainfrom
claude/toplevel-hook

Conversation

@tmashara

@tmashara tmashara commented Nov 2, 2026

Copy link
Copy Markdown
Owner

A route's own before or beforeShot still replaces the top-level one; that stays the default. It can now run the hook it replaced through ctx.topLevel:

export default {
  before: async (page) => {
    const accept = page.getByRole("button", { name: "Accept" });
    if (await accept.isVisible()) await accept.click();
  },
  routes: [
    {
      left: "/docs",
      before: async (page, { topLevel }) => {
        await topLevel?.();
        await page.click("[aria-label=Menu]");
      },
    },
  ],
};

topLevel is set only on a route's own hook when a different top-level hook of that kind exists. Otherwise it is undefined, hence the ?.(). It runs with the same page and { side, route, width }, inside the calling hook's step. A throw or a hang there is reported as that hook's error or timeout, the same as before.

Notes

  • shotId includes the top-level hooks' sources, so a --cache-right recording isn't replayed after only the top-level hook changes.
  • configHash changes for every config, because routes carry the new field. That's harmless: version() already invalidates reruns on every release. Accepted entries aren't keyed on it, so they're unaffected.
  • Docs: the Hook jsdoc, renderdiff.config.example.ts, docs/scoping.md and skills/renderdiff/reference.md.

Testing

  • typecheck, lint, format:check and npm test (362/362) pass.
  • Visual: the "before hooks" suite in tests/visual/guardrails.test.ts passes, including the new test "a route's own hook can run the top-level one as ctx.topLevel".
  • Unit: one shotId assertion checks that a different top-level hook gives a different key.

Refs #59

🤖 Generated with Claude Code

A route's own before or beforeShot still replaces the top-level one, but can
now run it with `await topLevel?.()`, inside the same timeout and hook error.
shotId includes the top-level hook's source.

Refs #59

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmashara
tmashara merged commit 9bc063d into main Nov 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant