Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
18a3656
chore(ci): move to next version [BUMP BETA] (#6171)
jbocce Jul 21, 2026
a079c98
chore(version): Update package versions to 3.14.0-beta.0 [skip ci]
ohif-bot Jul 21, 2026
ce2af86
fix(security): bump dependencies to fix security vulnerabilities (#6176)
jbocce Jul 23, 2026
d03f77a
chore(version): Update package versions to 3.14.0-beta.1 [skip ci]
ohif-bot Jul 23, 2026
482a95f
fix(security): update postcss to 8.5.22 (#6181)
jbocce Jul 24, 2026
1628ddb
chore(version): Update package versions to 3.14.0-beta.2 [skip ci]
ohif-bot Jul 24, 2026
93934db
fix(cornerstone): share one color LUT across a segmentation's viewpor…
TFRadicalImaging Jul 28, 2026
7b23f66
chore(version): Update package versions to 3.14.0-beta.3 [skip ci]
ohif-bot Jul 28, 2026
936c495
feat(customization): typed customization registry via AppTypes.Custom…
sedghi Jul 28, 2026
7f0f38f
chore(version): Update package versions to 3.14.0-beta.4 [skip ci]
ohif-bot Jul 28, 2026
f67929e
fix(core): report single-location multi-frame series as non-reconstru…
TFRadicalImaging Jul 28, 2026
a86d438
fix(cornerstone-dicom-sr): handle content-less imaging measurement re…
TFRadicalImaging Jul 28, 2026
1d2657d
chore(version): Update package versions to 3.14.0-beta.5 [skip ci]
ohif-bot Jul 28, 2026
f6d8469
fix(DicomWebDataSource): set correct cross-service URLs on DICOMweb c…
galenzo17 Jul 29, 2026
6985ca5
chore(version): Update package versions to 3.14.0-beta.6 [skip ci]
ohif-bot Jul 29, 2026
e442bbd
fix(security): update dependencies to fix brace-expansion vulnerabili…
jbocce Aug 3, 2026
6d81d97
chore(version): Update package versions to 3.14.0-beta.7 [skip ci]
ohif-bot Aug 3, 2026
2ba4153
test(segmentation): keep spline contours visible when drawing another…
GhadeerAlbattarni Aug 3, 2026
c7adb49
chore(version): Update package versions to 3.14.0-beta.8 [skip ci]
ohif-bot Aug 3, 2026
f552d9f
test(e2e): Add viewport text hiding for screenshot comparisons (#6142)
GhadeerAlbattarni Aug 5, 2026
cea9b7d
chore(version): Update package versions to 3.14.0-beta.9 [skip ci]
ohif-bot Aug 5, 2026
ef7a7c2
test(measurements): add DOM assertions for measurement annotation tex…
GhadeerAlbattarni Aug 6, 2026
6e155bb
chore(version): Update package versions to 3.14.0-beta.10 [skip ci]
ohif-bot Aug 6, 2026
8eb0a18
test(segmentation): add labelmap segment color change E2E tests. (#6054)
diattamo Aug 10, 2026
1448d31
chore(version): Update package versions to 3.14.0-beta.11 [skip ci]
ohif-bot Aug 10, 2026
d91a451
fix(security): update dependencies to fix security vulnerabilities (#…
jbocce Aug 11, 2026
e4098bb
chore(version): Update package versions to 3.14.0-beta.12 [skip ci]
ohif-bot Aug 11, 2026
32beb70
fix(segmentation): drop a removed representation from the panel (#6214)
wayfarer3130 Aug 17, 2026
6155c58
chore(version): Update package versions to 3.14.0-beta.13 [skip ci]
ohif-bot Aug 17, 2026
d7cf363
fix(hotkeys): hand keystrokes back to the browser inside modal dialog…
TFRadicalImaging Aug 21, 2026
42b6231
chore(version): Update package versions to 3.14.0-beta.14 [skip ci]
ohif-bot Aug 21, 2026
5fc3f14
test(contour): add Freehand and Spline contour drawing tool E2E tests…
diattamo Aug 24, 2026
cde349f
chore(version): Update package versions to 3.14.0-beta.15 [skip ci]
ohif-bot Aug 24, 2026
6b367e5
test(contour): Livewire contour draw coverage (#6206)
diattamo Aug 24, 2026
1f444f3
chore(version): Update package versions to 3.14.0-beta.16 [skip ci]
ohif-bot Aug 24, 2026
778fe4a
fix(security): update dependencies to fix security vulnerabilities (#…
jbocce Sep 1, 2026
5c79f03
chore(version): Update package versions to 3.14.0-beta.17 [skip ci]
ohif-bot Sep 1, 2026
d552da1
fix(security): Patch browserslist and fast-uri security vulnerabiliti…
jbocce Sep 3, 2026
179f04d
chore(version): Update package versions to 3.14.0-beta.18 [skip ci]
ohif-bot Sep 3, 2026
ef0811c
fix(dicom-pdf): guarantee the type of encapsulated documents (#6228)
wayfarer3130 Sep 3, 2026
6f91ae2
chore(version): Update package versions to 3.14.0-beta.19 [skip ci]
ohif-bot Sep 3, 2026
fac6ba3
docs(tests): add E2E contribution guidelines and require text-free vi…
diattamo Sep 4, 2026
53514b6
chore(version): Update package versions to 3.14.0-beta.20 [skip ci]
ohif-bot Sep 4, 2026
ab91811
feat(ReportDialog): say whether the save creates a new series or exte…
wayfarer3130 Sep 4, 2026
dc204f6
chore(version): Update package versions to 3.14.0-beta.21 [skip ci]
ohif-bot Sep 4, 2026
df78478
feat(customization): remove or replace the header's undo/redo via ohi…
salimkanoun Sep 4, 2026
12c32dd
chore(version): Update package versions to 3.14.0-beta.22 [skip ci]
ohif-bot Sep 4, 2026
d211a04
fix(rtstruct): prevent viewport from becoming blank on second load (#…
Belbin-GK Sep 4, 2026
8889c75
chore(version): Update package versions to 3.14.0-beta.23 [skip ci]
ohif-bot Sep 4, 2026
65588fd
test(contour): add smooth edges and merge operation E2E tests (#6236)
diattamo Sep 8, 2026
00f8ff7
chore(version): Update package versions to 3.14.0-beta.24 [skip ci]
ohif-bot Sep 8, 2026
105bd94
fix(security): update dependencies to address several vulnerabilities…
jbocce Sep 9, 2026
154fba9
chore(version): Update package versions to 3.14.0-beta.25 [skip ci]
ohif-bot Sep 9, 2026
7310bca
fix(sorting): order display sets by the creation date/time of their i…
wayfarer3130 Sep 10, 2026
2f2d015
chore(version): Update package versions to 3.14.0-beta.26 [skip ci]
ohif-bot Sep 10, 2026
8d7cb45
fix(report): correct the save of a revision into an existing series (…
TFRadicalImaging Sep 14, 2026
c29f6ae
chore(version): Update package versions to 3.14.0-beta.27 [skip ci]
ohif-bot Sep 14, 2026
3f13f1e
fix(Layout): apply the selected preset instead of restoring a stale g…
TFRadicalImaging Sep 14, 2026
a0ab6af
chore(version): Update package versions to 3.14.0-beta.28 [skip ci]
ohif-bot Sep 14, 2026
020a2bf
fix(ui-next): modify numeric and double slider inputs behavior to all…
eloisalgado Sep 15, 2026
1ec0134
chore(version): Update package versions to 3.14.0-beta.29 [skip ci]
ohif-bot Sep 15, 2026
f9c25f8
feat: React 19 + React Compiler, compiler-era cleanup, and rsbuild pr…
sedghi Sep 17, 2026
d333927
chore(version): Update package versions to 3.14.0-beta.30 [skip ci]
ohif-bot Sep 17, 2026
8f770e6
test(contour): add fill, outline and opacity contour display coverage…
diattamo Sep 18, 2026
e3d0d57
chore(version): Update package versions to 3.14.0-beta.31 [skip ci]
ohif-bot Sep 18, 2026
bf742a5
fix(mode-route): validate studies after route init to support custom …
Sofien-Sellami Sep 18, 2026
41c03bc
chore(version): Update package versions to 3.14.0-beta.32 [skip ci]
ohif-bot Sep 18, 2026
c68b70c
chore(ci): harden the workflow that runs on the self-hosted runner (#…
jbocce Sep 21, 2026
a300462
chore(version): Update package versions to 3.14.0-beta.33 [skip ci]
ohif-bot Sep 21, 2026
9e10bee
fix(sr): skip derived display sets when placing SR measurements (#6278)
TFRadicalImaging Sep 21, 2026
28dad83
chore(version): Update package versions to 3.14.0-beta.34 [skip ci]
ohif-bot Sep 21, 2026
9bbfba1
fix(security): update adm-zip version (#6286)
jbocce Sep 21, 2026
aafb6e8
chore(version): Update package versions to 3.14.0-beta.35 [skip ci]
ohif-bot Sep 21, 2026
3ff225c
chore: remove dead husky config from package.json (#6291)
wayfarer3130 Sep 23, 2026
9753879
chore(version): Update package versions to 3.14.0-beta.36 [skip ci]
ohif-bot Sep 23, 2026
6291bda
fix(netlify): remove the unused runtime.txt Python pin (#6302)
jbocce Sep 24, 2026
9ba15ec
chore(version): Update package versions to 3.14.0-beta.37 [skip ci]
ohif-bot Sep 24, 2026
f14f79e
fix(release): create GitHub Releases from a workflow again (#6316)
jbocce Sep 30, 2026
e20679f
chore(version): Update package versions to 3.14.0-beta.38 [skip ci]
ohif-bot Sep 30, 2026
7e73f81
chore(deps): update pnpm to 12.8.1 and i18next to 19.9.2 (#6322)
wayfarer3130 Sep 30, 2026
119f997
chore(version): Update package versions to 3.14.0-beta.39 [skip ci]
ohif-bot Sep 30, 2026
f401e15
fix(security): update dependencies for axios and image-size. (#6327)
jbocce Sep 30, 2026
8eb5a8e
chore(version): Update package versions to 3.14.0-beta.40 [skip ci]
ohif-bot Sep 30, 2026
8f2f150
fix(ci): stop deferring fork PRs that only touch package.json or the …
jbocce Oct 2, 2026
f06cf8b
chore(version): Update package versions to 3.14.0-beta.41 [skip ci]
ohif-bot Oct 2, 2026
39343e6
docs(playwright): move E2E contribution guide into the docs site (#6311)
diattamo Oct 5, 2026
eb82da8
chore(version): Update package versions to 3.14.0-beta.42 [skip ci]
ohif-bot Oct 5, 2026
2807c36
feat(undo): add optional undo/redo history size limit and disable but…
rleisti Oct 5, 2026
45c4a81
chore(version): Update package versions to 3.14.0-beta.43 [skip ci]
ohif-bot Oct 5, 2026
bcde2b0
chore(security): ignore GHSA-ch52-4w7c-c8xp and GHSA-vfj7-8cjw-p6xm i…
jbocce Oct 5, 2026
05f78b8
chore(version): Update package versions to 3.14.0-beta.44 [skip ci]
ohif-bot Oct 5, 2026
04a60bd
fix(seg): fallback to buffer-based loader when PerFrameFunctionalGrou…
igoroctaviano Oct 6, 2026
bfbb25a
fix(seg): try bulk data fetch for PerFrameFunctionalGroupsSequence be…
igoroctaviano Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
122 changes: 122 additions & 0 deletions .agents/skills/ohif-react-compiler/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
name: ohif-react-compiler
description: Rules and workflow for editing React code in the OHIF Viewer, which runs React 19 with the React Compiler enabled and two CI gates that fail when a file stops compiling or lint counts move. Use this skill whenever you create or change anything under platform/*/src, extensions/*/src or modes/*/src that contains JSX or a React hook — including small edits, refactors, and requests like "add a useCallback here" — even if the user does not mention React 19, the compiler, or memoization.
---

# OHIF React Compiler

This repo compiles its React with the React Compiler. Most of what that means
is enforced by tooling; this skill tells you how to work with that tooling and
records the decisions already made so you do not relitigate them.

## How this repo is set up

- React 19. `ref` is an ordinary prop. There is no `forwardRef`, no
`propTypes`, and manual memoization is unnecessary.
- Which directories the compiler applies to is defined once, in
`react-compiler.scope.cjs` at the repo root. Read it rather than assuming; the
build, the lint rules and the coverage gate all take their scope from it, and
adding a directory to its `excluded` list takes that directory out of all
three together. Files carrying a `'use no memo'` directive at the top are
skipped regardless of directory.
- Two CI gates hold the current state and fail in **both** directions — when
things get worse, and when things get better but the budget was not tightened.

| gate | command | budget file |
| ----------------- | ------------------------------- | ---------------------------------- |
| compiler coverage | `pnpm run compiler:coverage:ci` | `.react-compiler-budget.json` |
| compiler lint | `pnpm run lint:compiler:ci` | `.react-compiler-lint-budget.json` |

## The loop — after every React edit

1. Lint the file you touched:
`npx eslint --config eslint.config.mjs --no-config-lookup <file>`
2. `pnpm run compiler:coverage:ci` — did any file start refusing or opt out?
3. `pnpm run lint:compiler:ci` — did the error or warning counts move?
4. If either gate says the counts **improved**, tighten the budget to the exact
values it prints, in the same commit. Never loosen a budget to make CI pass.

Step 1 is fast and catches most problems. Steps 2 and 3 are what CI runs.

## Hard rules — CI enforces these

- **No `forwardRef`.** Accept `ref` as a regular prop. An ESLint
`no-restricted-syntax` rule fails on it.
- **No `prop-types`.** Use TypeScript types. An ESLint `no-restricted-imports`
rule fails on the import.
- **No `eslint-disable` on any `react-hooks/*` rule.** The compiler treats a
suppression as a `Suppression` bailout and refuses the whole function. Fix the
code instead.
- **Do not mutate props, and do not read or write `ref.current` during render.**
Both are refusals (`Immutability`, `Refs`). Compute into a local; move ref
access into an effect or event handler.
- **No new `'use no memo'` without all three of:** a reproduced failure, a
comment above the directive saying exactly what broke, and the file added to
`fileOptOuts` in `.react-compiler-budget.json`. An opt-out is a last resort
with a paper trail, not a way to make a refusal go away.

## Decisions already made

- **Do not add `useMemo`, `useCallback` or `React.memo`.** The compiler derives
its own memoization from the code and erases hand-written wrappers from the
output. Adding one gains nothing and gives the compiler a dependency list it
has to verify.
- **Removing an existing wrapper: diff the compiled output.** Emit the file
before and after (see "Reading emitted code"). Identical output means nothing
to test. If `const $ = _c(n)` disappears from the output, the compiler has
started refusing — the wrapper was load-bearing; stop and look.
- **A refusal gets fixed, not hidden.** Read the category the gate prints and
see [references/refusal-categories.md](references/refusal-categories.md) for
what it means and the usual fix.
- **Public API changes get a migration note** in
`platform/docs/docs/migration-guide/3p13-to-3p14/`.
- **Components that read or mutate cornerstone3D state during render** are the
one class that genuinely cannot be compiled yet. That is what the existing
opt-outs under `extensions/cornerstone/src/Viewport/` are. Do not extend that
set casually; each one has an issue to remove it.

## Reading the gate output

`compiler:coverage:ci` prints every refusal as file, line, category and reason:

```
extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx:50
Todo: (BuildHIR::lowerExpression) Handle ||= operators in AssignmentExpression
```

The line points at the offending expression, not the function. A block titled
"Refusals inside opt-out files" is informational — those files are not being
memoized anyway.

`lint:compiler:ci` printing **"Budget is stale: the counts improved"** is not an
error to work around. It is telling you to write the new numbers into the
budget file.

## Reading emitted code

Use this in two situations only: after fixing a refusal, to confirm the function
now compiles; and before and after removing a `useMemo` or `useCallback`, to see
whether the change altered anything. A new component that passes the gates does
not need it.

To see what the compiler actually produced for a file:

```bash
node .agents/skills/ohif-react-compiler/assets/emit.mjs <file>
```

It compiles that one file with the repo's own babel config, compiler included,
and prints the result. To check whether an edit changed anything the compiler
emits, run it before and after and diff the two outputs — identical output
means identical runtime behaviour, so there is nothing to test.

What to look for: `const $ = _c(n)` means the function compiled; `if ($[i] !== x)`
is a cache guard on input `x`; `t0`, `t1`… are compiler temporaries. A breakpoint
inside a guard fires once and then stops — that is the cache hitting, not a bug.

## Authoritative sources

- [Rules of React](https://react.dev/reference/rules) — what the compiler assumes.
- [eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks)
— the compiler's diagnostics as lint rules; this repo uses `recommended-latest`.
- [React Compiler](https://react.dev/learn/react-compiler) — directives, opt-outs.
24 changes: 24 additions & 0 deletions .agents/skills/ohif-react-compiler/assets/emit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Print what the React Compiler emits for one file.
//
// node .agents/skills/ohif-react-compiler/assets/emit.mjs <file>
//
// Uses the repo's own babel config (compiler included), found by walking up
// from the target file, so it works from any working directory. To see whether
// an edit changed anything the compiler produces, run it before and after and
// diff the two outputs. Identical output means identical runtime behaviour.
import { createRequire } from 'node:module';
import path from 'node:path';

const file = path.resolve(process.argv[2] ?? '');
if (!process.argv[2]) {
console.error('usage: node emit.mjs <file>');
process.exit(2);
}

const babel = createRequire(file)('@babel/core');
const { code } = babel.transformFileSync(file, {
filename: file,
cwd: path.dirname(file),
rootMode: 'upward',
});
console.log(code);
152 changes: 152 additions & 0 deletions .agents/skills/ohif-react-compiler/references/refusal-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Refusal categories

When the React Compiler will not memoize a function it emits it unchanged and
reports a category and a reason. `pnpm run compiler:coverage:ci` prints both,
with the line of the offending expression. The reason string is the compiler's
own and is authoritative; this file explains what each category usually means
in this codebase and how it has been fixed before.

## Sources

- **The category names are the compiler's own.** This file covers the nine this
codebase has produced; the compiler defines more, and the gate prints
whichever one it hits.
- **Lint rule names and which are enabled:**
[eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks).
This repo uses the `recommended-latest` preset, which leaves some of the
plugin's rules off. Every rule named below was checked against the installed
plugin.
- **How to read a diagnostic:**
[React Compiler docs](https://react.dev/learn/react-compiler) and the
[Rules of React](https://react.dev/reference/rules) the compiler assumes.
- **The "how it has been fixed" guidance** comes from fixes made in this repo
during the React 19 migration, not from documentation. `Hooks`, `Globals` and
`UseMemo` were cleared early in that work, so their fix guidance is briefer
and less tested than the others'.

Two things to know first:

- **A function reports one category at a time.** Fixing it often exposes a
different one underneath. Recompile after every fix and expect a second
round.
- **Most categories have no lint rule that fires here.** `PreserveManualMemo`
in particular has a rule enabled as an error that has never once fired on a
real case. Only compiling finds these — which is why the coverage gate exists.

## Immutability

*"This value cannot be modified"*, *"Cannot access variable before it is
declared"*.

Something is being written to that the compiler needs to treat as read-only:
a prop, a value captured by a closure, or a variable referenced before its
declaration in a circular pair of callbacks.

Fix by computing into a new local instead of assigning into the existing
object, and by breaking circular references between callbacks (for example an
`AbortController` for listener teardown rather than each handler naming the
other). Check every downstream reader before changing a mutation — today they
observe the mutated object.

Lint: `react-hooks/immutability` reports some but not all of these.

## Refs

*"Cannot access refs during render"*.

`ref.current` is read or written in the render body rather than in an effect or
an event handler. The compiler cannot cache across a value it cannot see change.

Fix by moving the access into a `useEffect` or a handler. Where a ref is used to
carry identity across renders synchronously — "reset X when Y changed, before
anything renders" — the fix is a design change, not a mechanical one. See the
`Mode.tsx` issue for a case where the recommendation is to leave it.

Lint: `react-hooks/refs`.

## PreserveManualMemo

*"Existing memoization could not be preserved"*.

A hand-written `useMemo` or `useCallback` has a dependency list narrower than
what its body actually reads. The compiler infers the real dependencies, finds
they differ from what was written, and refuses the whole function rather than
silently change behaviour.

Fix by completing the dependency list, or by deleting the wrapper. In a
compiled file deleting is usually safe — the compiler re-derives the
memoization — but diff the compiled output before and after to be sure.

Lint: `react-hooks/preserve-manual-memoization` is enabled and has never fired
on a real instance here. Do not rely on it.

## Suppression

An `eslint-disable` comment for a `react-hooks/*` rule is present in the
function. The compiler treats the suppression as a signal that the code
knowingly breaks a rule, and refuses.

Fix by removing the disable comment and fixing what it was hiding — usually an
incomplete dependency list.

Lint: `react-hooks/rule-suppression` exists but is not in the preset this repo
uses.

## RenderSetState

A state setter is called during render rather than in an effect or handler.

Fix by deriving the value instead of storing it, or by moving the call to where
it belongs. If the setter is guarded so it only fires on a real change, restate
that as derived state.

Lint: `react-hooks/set-state-in-render`.

## Hooks

A hook is called conditionally, in a loop, or after an early return, so the
compiler cannot establish a fixed hook order.

Fix by moving every hook above the first `return` and out of any branch. This
is the same rule `rules-of-hooks` enforces, and it has always applied.

Lint: `react-hooks/rules-of-hooks`; the compiler-specific rule
`react-hooks/hooks` exists but is not in the preset.

## Globals

A module-scope value is mutated during render.

Fix by moving the mutation into an effect or handler, or by lifting the value
into state or context so React owns it.

Lint: `react-hooks/globals`.

## UseMemo

`useMemo` is called in a shape the compiler will not handle — for example with
something other than a plain function as its first argument.

Fix per the reason string; usually the wrapper can simply be removed.

Lint: `react-hooks/use-memo`.

## Todo

*"(BuildHIR::lowerExpression) Handle …"*.

Not a problem with your code. The compiler's front end has not implemented a
syntax form you used. The one instance in this codebase is a logical assignment
operator, `||=`.

Fix by rewriting the construct longhand (`x = x || y`). This category shrinks
with each compiler release.

Lint: `react-hooks/todo` reports these verbatim but is not in the preset.

## Which lint rules are actually on

This repo uses `eslint-plugin-react-hooks` `recommended-latest`, which does not
enable every rule the plugin ships. The ones that would report `Todo`,
`Suppression`, and the compiler-specific `hooks` are off. So the linter being
clean does not mean the compiler is happy; only the coverage gate tells you that.
27 changes: 14 additions & 13 deletions .agents/skills/ohif-test-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,34 +243,35 @@ Reach for the cheapest *faithful* signal, in this order:

1. **A faithful DOM/SVG/state signal exists → assert on it.** Panel counts, dialog and
overlay text, enabled/disabled state, and any overlay that renders as SVG (a vector
overlay's color is readable via `getSvgAttribute`) all have a DOM representation — assert
on it directly, no screenshot.
2. **The thing under test is painted onto the WebGL canvas with no DOM representation → a
screenshot is correct and required.** Raster output on the canvas exposes no attribute to
read for a painted pixel. Scope a `checkForScreenshot` to the viewport (pane or grid) and
assert it — this is the right tool, not a last resort, whenever what you're verifying is
the rendered canvas itself.
overlay's color is readable via `getSvgAttribute`) are all readable from the DOM —
assert on them directly, no screenshot.
2. **The thing under test exists only as pixels on the WebGL canvas → a screenshot is
correct and required.** A painted pixel exposes no element or attribute to read. Capture
a viewport pane with `checkForViewportScreenshot`, or scope a `checkForScreenshot` to the
grid — this is the right tool, not a last resort, whenever what you're verifying is the
rendered canvas itself.
3. **Never substitute a service/state read for a render assertion.** Reading a service's
state (any `window.services...`) asserts the *data model*, not the pixels the user sees —
it passes even when rendering is broken. `page.evaluate(() => window.services...)` is an
escape hatch for *setup*, not for *appearance* assertions.

For anything drawn onto the WebGL canvas with no DOM signal, compare a screenshot scoped to a specific viewport or the viewport grid:
For a screenshot comparison scoped to a specific viewport, use `checkForViewportScreenshot` — it hides the viewport's overlay text for the capture:

```ts
await checkForScreenshot({
await checkForViewportScreenshot({
page,
locator: viewportPageObject.grid, // scope to the viewport grid — not the whole page
viewport: activeViewport, // captures the viewport pane with its text hidden
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```

`checkForScreenshot` retries up to 10 times at 500 ms intervals. Use `screenShotPaths.<category>.<name>` rather than a hand-typed string — the tree of valid keys lives in `tests/utils/screenShotPaths.ts`.
Both helpers retry up to 10 times at 1250 ms intervals by default (`attempts` and `delay` are configurable) (`checkForViewportScreenshot` delegates to `checkForScreenshot`; use the latter directly only for non-viewport locators such as the grid or a panel). Use `screenShotPaths.<category>.<name>` rather than a hand-typed string — the tree of valid keys lives in `tests/utils/screenShotPaths.ts`.

Rules (apply to all new screenshot assertions):

- **Use the object form.** The positional form is legacy; don't introduce it in new code, and don't treat existing positional-form usage as a pattern to copy.
- **Never screenshot the full app.** Full-page screenshots include panels, toolbars, and dialogs that drift independently of what's under test and make baselines fragile. Scope by passing a `locator` — `viewportPageObject.grid` for the grid, or a specific viewport pane. A bare `normalizedClip: { x: 0, y: 0, width: 1, height: 1 }` with no `locator` is **not** scoping — it clips to the full page. Use `normalizedClip` only to target a sub-region *of a locator* (e.g. a scrollbar strip). If you reach for `fullPage: true`, stop and pick a locator.
- **No text in baselines.** Overlay text (date, series description, W/L, slice index) drifts with data, locale, and font rendering, so a baseline that contains it is fragile — new viewport baselines must be text-free. Capture viewports through `checkForViewportScreenshot`, which hides all viewport text for the shot; a raw `checkForScreenshot` on a viewport pane bakes the text in.
- **Never screenshot the full app.** Full-page screenshots include panels, toolbars, and dialogs that drift independently of what's under test and make baselines fragile. Scope by passing a `locator` — `viewportPageObject.grid` for the grid, or a specific viewport pane. A bare `normalizedClip: { x: 0, y: 0, width: 1, height: 1 }` with no `locator` is **not** scoping — it clips to the full page. Use `normalizedClip` only to target a sub-region *of a locator* (e.g. a scrollbar strip). `fullPage: true` only takes effect when no `locator` is passed — that *is* the full-app capture this rule forbids, so pass a `locator` instead. (Through `checkForViewportScreenshot` the flag is inert: the capture is always scoped to the viewport pane.)
- **Do not tune `maxDiffPixelRatio` or `threshold`** to make a screenshot pass. If a baseline mismatches, regenerate it after a human review of the diff, or fix the underlying flake.

## Playwright config facts worth remembering
Expand Down Expand Up @@ -323,7 +324,7 @@ Before returning a generated OHIF test, confirm all items:
3. Uses normalized viewport interactions (`normalizedClickAt` / `normalizedDragAt`) unless there is a strong reason otherwise.
4. Uses a valid canonical StudyInstanceUID and compatible mode.
5. Handles hydration or measurement tracking prompts when the workflow requires them.
6. Uses the faithful signal for each assertion — DOM/SVG where the result has a DOM representation, a viewport-scoped screenshot when what's verified is canvas-only raster output, and never a `window.services` state read in place of a render check. Any `checkForScreenshot` call uses the object form, scoped via a `locator` (viewport pane or grid) — no full-app screenshots.
6. Uses the faithful signal for each assertion — DOM/SVG where the result is readable from the DOM, a viewport-scoped screenshot when what's verified exists only as pixels on the canvas, and never a `window.services` state read in place of a render check. Any `checkForScreenshot` call uses the object form, scoped via a `locator` (viewport pane or grid) — no full-app screenshots.
7. Replaces `page.waitForTimeout(...)` after viewport-rendering actions with `waitForViewportRenderCycle(page)` (started before the action) — keeps `waitForTimeout` only for non-render waits like the hydration prompt in `beforeEach`.
8. If execution was skipped, states that explicitly and provides concrete run commands.
9. Every application control is reached through a page object — no raw `getByTestId`/`getByRole` in the spec for buttons, menus, dialogs, or fields. Any control not already covered was added to the right page object (or a new one), with a source `data-cy` if it lacked one.
Expand Down
Loading