Skip to content

feat: Add customization URL parameter#5992

Open
wayfarer3130 wants to merge 31 commits into
masterfrom
feat/customization-url-parameter
Open

feat: Add customization URL parameter#5992
wayfarer3130 wants to merge 31 commits into
masterfrom
feat/customization-url-parameter

Conversation

@wayfarer3130

@wayfarer3130 wayfarer3130 commented May 4, 2026

Copy link
Copy Markdown
Contributor

Context

In order to allow custom versions of OHIF to be defined/added without having to rebuild OHIF, it is necessary to have a customization framework that can load dynamic modules. This has been added as a customization= parameter.

Changes & Results

Added a customization handler for the customization= parameter
Added a requires= export in the loaded global customizations to allow customizations to depend on other ones, eg veterinary depends on veterinaryOverlay
Add an example customization to test with, the start of a veterinary example.

URL customization examples (?customization=)

This PR ships a set of runtime-loadable customization modules under platform/app/public/customizations/ to demonstrate the ?customization= framework. Each is a plain ES module whose global payload is applied as a global customization at bootstrap (using immutability-helper commands, so it extends the built-in defaults rather than replacing them). Append &customization=<name> to any viewer URL to try one — e.g. /viewer?StudyInstanceUIDs=<uid>&customization=smoothRotate.

?customization= What it demonstrates
smoothRotate Adding a toolbar button at runtime
ctPresets Overriding window/level presets
measurementLabels Driving measurement labelling
veterinaryOverlay Replacing the viewport overlay layout
veterinary A requires dependency chain (pulls in veterinaryOverlay)

smoothRotate — add a toolbar button

Adds a Smooth Rotate button to the More Tools menu that activates the cornerstone PlanarRotate tool (drag to rotate the image to any angle, unlike the fixed-90° Rotate Right).

This is enabled by a supporting change: the basic/longitudinal viewers no longer hard-code their toolbar — the cornerstone extension now registers it as customizations (cornerstone.toolbarButtons = button defs, cornerstone.toolbarSections = the section→ids layout), and the modes reference those by name in onModeEnter. The module $pushes a button definition and its id into the MoreTools section. PlanarRotate is registered as an available tool by default. Because the defaults register at default scope and the URL module applies at global scope, the push extends the built-in toolbar instead of clobbering it.

ctPresets — site-specific window/level presets

$merges a custom CT preset list into cornerstone.windowLevelPresets. The new presets appear in the window-level menu for CT; presets for other modalities (PT, etc.) are preserved because only the CT key is replaced.

measurementLabels — predefined measurement labels

Sets measurementLabels with labelOnMeasure: true and an exclusive item list, so completing a measurement prompts the user to pick a label (Head/Shoulder/Knee/Toe) instead of free-typing.

veterinaryOverlay — replace the viewport overlay

Replaces viewportOverlay.topLeft / viewportOverlay.topRight with a veterinary-style demographics layout (species/breed), demonstrating overlay customization.

veterinary — dependency chains

A near-empty module that declares requires: ['veterinaryOverlay'], showing how one module can pull in another so layered packs (base → shared → site-specific) load in order.

Security notes

  • Loading is allowlisted: values must normalize to /prefix/name, full URLs and path traversal (including percent-encoded forms like %2e%2e) are rejected, and the resolved URL is verified to stay within the configured prefix directory.
  • Modules are applied once at bootstrap and deduped for the page session; they aren't re-imported on SPA navigation.

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • [] My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] OS:
  • [] Node version:
  • [] Browser:

Summary by CodeRabbit

Release Notes

  • New Features

    • Added URL-driven customization module support, including a veterinary overlay pack (?customization=veterinaryOverlay).
  • Improvements

    • Overlay item values are now properly formatted, and empty/null values are hidden.
    • Query preservation now keeps customization-related keys and preserves repeated values.
    • Viewer header query preservation now includes customization context.
  • Documentation

    • Expanded customization service docs with security/behavior details.
  • Tests

    • Added Jest coverage for customization URL loading/validation and Playwright coverage for veterinary overlay rendering.
  • Refactor

    • Adjusted customization initialization during app startup.

@netlify

netlify Bot commented May 4, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 271be36
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a3c3004021aeb0008de9c68
😎 Deploy Preview https://deploy-preview-5992--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread platform/core/src/services/CustomizationService/resolve.test.ts Dismissed
Comment thread platform/core/src/services/CustomizationService/resolve.test.ts Fixed
@cypress

cypress Bot commented May 4, 2026

Copy link
Copy Markdown

Viewers    Run #6428

Run Properties:  status check failed Failed #6428  •  git commit 271be3642e: Merge branch 'master' into feat/customization-url-parameter
Project Viewers
Branch Review feat/customization-url-parameter
Run status status check failed Failed #6428
Run duration 02m 25s
Commit git commit 271be3642e: Merge branch 'master' into feat/customization-url-parameter
Committer Bill Wallace
View all properties for this run ↗︎

Test results
Tests that failed  Failures 12
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 16
Tests that passed  Passing 0
View all changes introduced in this branch ↗︎

Tests for review

Failed  measurement-tracking/OHIFCornerstoneToolbar.spec.js • 1 failed test

View Output Video

Test Artifacts
OHIF Cornerstone Toolbar > checks if all primary buttons are being displayed Test Replay Screenshots Video
Failed  OHIFVideoDisplay.spec.js • 2 failed tests

View Output Video

Test Artifacts
OHIF Video Display > checks if series thumbnails are being displayed Test Replay Screenshots Video
OHIF Video Display > performs double-click to load thumbnail in active viewport Test Replay Screenshots Video
Failed  measurement-tracking/OHIFMeasurementPanel.spec.js • 1 failed test

View Output Video

Test Artifacts
OHIF Measurement Panel > checks if Measurements right panel can be hidden/displayed Test Replay Screenshots Video
Failed  customization/HangingProtocol.spec.js • 1 failed test

View Output Video

Test Artifacts
OHIF HP > Should display 3 up Test Replay Screenshots Video
Failed  measurement-tracking/OHIFCornerstoneHotkeys.spec.js • 1 failed test

View Output Video

Test Artifacts
OHIF Cornerstone Hotkeys > checks if hotkeys "R" and "L" can rotate the image Test Replay Screenshots Video

The first 5 failed specs are shown, see all 11 specs in Cypress Cloud.

Comment thread platform/core/src/services/CustomizationService/resolve.ts Outdated
Comment thread platform/app/src/utils/preserveQueryParameters.ts
@wayfarer3130 wayfarer3130 requested a review from sedghi May 7, 2026 22:14

@sedghi sedghi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not fully convinced the added value of this PR justifies the new security surface yet. This introduces URL-driven runtime JavaScript loading via ?customization=, which means a shared link can change viewer behavior and, depending on deployment config, potentially load executable code into the same browser context as OHIF. The validation does block obvious arbitrary URLs and path traversal, so this is not an immediate “any URL can execute code” issue. But the security boundary becomes the configured customization prefix and whoever can publish files there. If that directory/CDN is writable by the wrong party, this could become XSS-equivalent: token/session access, DICOM metadata exposure, UI manipulation, report tampering, or authenticated API abuse from the victim’s browser.

My current view is that this level of runtime configurability may be better kept in downstream forks or deployment-specific builds, where the deploying team can own the threat model and hosting controls explicitly. I’m not sure it should become a default upstream capability.

@wayfarer3130

Copy link
Copy Markdown
Contributor Author

I’m not fully convinced the added value of this PR justifies the new security surface yet. This introduces URL-driven runtime JavaScript loading via ?customization=, which means a shared link can change viewer behavior and, depending on deployment config, potentially load executable code into the same browser context as OHIF. The validation does block obvious arbitrary URLs and path traversal, so this is not an immediate “any URL can execute code” issue. But the security boundary becomes the configured customization prefix and whoever can publish files there. If that directory/CDN is writable by the wrong party, this could become XSS-equivalent: token/session access, DICOM metadata exposure, UI manipulation, report tampering, or authenticated API abuse from the victim’s browser.

My current view is that this level of runtime configurability may be better kept in downstream forks or deployment-specific builds, where the deploying team can own the threat model and hosting controls explicitly. I’m not sure it should become a default upstream capability.

If the CDN is writable by the wrong party, it doesn't matter what you do, they can replace the entire OHIF source control. At that point you are completely open.

What about adding a user configuration option to specifically and manually add customization prefixes rather than allowing it to be done via customization? That way we can default to one customization deploy somewhere that we control for the demonstration deployments, making note that is intended for demo purposes only, and same-host http /customization/ prefix path options so that we can deploy with a fixed deployment?

It is clear the advisory board wants SOMETHING that allows dynamic loading. I agree it needs to be controlled, but it also has to be external to the build process of OHIF, otherwise we will never meet the goals of allowing OHIF to be customized by non-developers.

Some other things we could consider:

  1. Allow JSON customizations from controlled locations for full setup
  2. Allow JS customizations to be loaded from given path names, but require a SHA sum to match. The list of valid SHA sums could come from a fixed list of valid/verified items
  3. Allow users to "add" new locations for SHA sum validators

That value of this is clearly extremely high given how many people on the meeting wanted something better. The only question becomes how to make it reasonably safe. It isn't fully safe, but neither is OHIF in the current configuration.

@wayfarer3130

Copy link
Copy Markdown
Contributor Author

@sedghi
Thanks Alireza — I agree with this concern, and I think your framing is right. The issue is not just whether the current validation blocks obvious bad paths or arbitrary URLs. The larger issue is that the current PR creates a new runtime code-loading surface where a URL-controlled value can affect what JavaScript gets loaded into the same browser context as OHIF.

I think we should change the direction of this PR to avoid that for the first version.

What I would propose instead is a narrower model:

  1. Executable code must still be trusted at build time.
  2. pluginConfig.json remains the source of truth for which executable modes/plugins/extensions/customizations are allowed.
  3. Any JavaScript module that can be loaded by a customization must be referenced from pluginConfig.json, resolved during the build, and included in the build output.
  4. ?customization=<name> may select a customization by name, but it must not be able to introduce a new JavaScript file, package, module specifier, URL, registry source, or version at runtime.
  5. Customizations in public/customization/ should be data-only for now, for example JSON/JSONC/JSON5, and should only be able to reference capabilities that were already approved in the build.

One nuance I want to preserve is that a customization should still be able to add existing build-approved modes/plugins/extensions to the active load list, even if they were not part of the original default load list. In other words, the customization can choose from a build-time-approved set, but cannot expand that set at runtime.

The lifecycle would be something like:

  • plugins: if referenced by the customization and present in the build-approved set, they can be loaded immediately;
  • modes: if referenced by the customization and present in the build-approved set, they can be added during startup;
  • extensions: if referenced by a selected mode/customization and present in the build-approved set, they can be loaded when that mode is loaded.

So the runtime customization can change which already-bundled capabilities become active, but it cannot cause the viewer to fetch or execute new code that was not already included by the build.

That does make this less flexible than the original PR, but I think it gives us a much cleaner security boundary:

  • shared links can select among known customizations;
  • shared links cannot introduce new executable code;
  • the trust boundary stays with the build/deployment process;
  • deployments do not need to solve runtime code signing, hashing, CDN trust, or remote plugin provenance yet;
  • if a customization references something not included in the build-time allowlist, the viewer should fail closed with a clear error.

I also agree that a full remote plugin ecosystem is a separate design problem. Eventually we may want a trusted plugin catalog, immutable versions, hashes/signatures, provenance, and tighter policy controls. But I do not think this PR needs to solve that yet. For this PR, I think the safer target is: runtime customization can activate/configure build-approved capabilities, but cannot load arbitrary runtime JavaScript.

Would that address your main concern if I rework the PR in that direction?

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds URL-driven customization module loading to CustomizationService via ?customization= query parameters, with security validation, URL resolution, depth-first dependency loading, and per-page deduplication. It moves customizationService.init to appInit, updates query parameter preservation to support multi-values and service-provided keys, adds a formatValue display utility used across overlay components, and ships example veterinary overlay modules with an E2E test.

Changes

URL-driven customization module loading

Layer / File(s) Summary
formatValue utility and overlay rendering fixes
platform/core/src/utils/formatValue.js, platform/core/src/utils/formatValue.test.js, platform/core/src/utils/index.ts, extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx, extensions/default/src/customizations/overlayItemCustomization.tsx
Adds a formatValue helper that normalizes null, string, DICOM Alphabetic objects, and primitives. Wires it into both overlay components to suppress null/empty renders and avoid raw [object Object] output. Includes unit tests.
URL customization validation, resolution, and payload extraction
platform/core/src/services/CustomizationService/customizationUrlTypes.ts, platform/core/src/services/CustomizationService/customizationUrlDefaults.ts, platform/core/src/services/CustomizationService/validate.ts, platform/core/src/services/CustomizationService/validate.test.ts, platform/core/src/services/CustomizationService/resolve.ts, platform/core/src/services/CustomizationService/resolve.test.ts, platform/core/src/services/CustomizationService/getUrlCustomizationModulePayload.ts, platform/core/src/services/CustomizationService/customizationUrl.ts, platform/core/src/services/index.ts
Introduces the URL customization security subsystem: type interfaces, default policy constants, parse/normalize/validate functions rejecting traversal/full URLs/unknown prefixes, resolveCustomizationUrl for safe URL construction, getUrlCustomizationModulePayload for normalizing imported module shapes, and the customizationUrl.ts public facade. Backed by unit test suites covering validation logic, URL resolution, and edge cases.
CustomizationService URL loading methods and init deduplication
platform/core/src/services/CustomizationService/CustomizationService.ts, platform/core/src/services/CustomizationService/CustomizationService.init.test.ts, platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts
Adds applyWindowUrlCustomizations, applyCustomizationUrlSearchParams, requires, and getValue methods. Changes init() to preserve default customizations across mode resets and merge each extension's customization module at most once per page via _extensionCustomizationModuleApplied. Implements depth-first dynamic import with per-page deduplication and strict/lenient error handling for invalid or missing modules.
App bootstrap wiring and preserveQueryParameters updates
platform/app/src/App.tsx, platform/app/src/appInit.js, platform/app/src/utils/preserveQueryParameters.ts, platform/app/src/utils/preserveQueryParameters.test.ts, extensions/default/src/ViewerLayout/ViewerHeader.tsx, platform/app/src/hooks/useStudyListStateSync.ts
Moves customizationService.init to appInit (after extension registration) and calls applyWindowUrlCustomizations(). Updates preserveQueryParameters/preserveQueryStrings to support multi-values via getAll, add customization to preserved keys, and accept service-provided extra keys. Passes customizationService into preserveQueryParameters from ViewerHeader. Types query accumulator to support string[] values.
Veterinary overlay modules and E2E test
platform/app/public/customizations/veterinary.js, platform/app/public/customizations/veterinaryOverlay.js, tests/utils/visitStudy.ts, tests/utils/index.ts, tests/Customization.spec.ts
Adds veterinary.js (requires-chain entry point) and veterinaryOverlay.js (overlay rows for PatientName, PatientID, StudyDate, PatientSpecies, PatientBreed) as URL-loadable customization examples. Adds visitStudyOptions helper with optional customization parameter. Playwright test verifies the overlay renders a horse patient name without [object Object].
Documentation and minor fixes
platform/docs/docs/platform/services/customization-service/specificCustomizations.md, platform/docs/docs/platform/services/customization-service/customizationService.md, platform/core/src/services/MultiMonitorService.ts
New specificCustomizations.md documents ohif.preserveCustomizationKeys, ohif.customizationUrl, security model, requires chaining, and SPA bootstrap behavior. Adds customization area links to customizationService.md. Removes spurious async keyword from MultiMonitorService.onModeEnter.

Sequence Diagram(s)

sequenceDiagram
  participant AppInit as appInit.js
  participant CustomizationService
  participant parseCustomizationParams
  participant validateCustomizationRequests
  participant resolveCustomizationUrl
  participant importFn as dynamic import()
  participant setCustomizations

  AppInit->>CustomizationService: init(extensionManager)
  AppInit->>CustomizationService: applyWindowUrlCustomizations()
  CustomizationService->>parseCustomizationParams: window.location.search → string[]
  parseCustomizationParams-->>CustomizationService: raw names
  CustomizationService->>validateCustomizationRequests: raws + policy → ValidationResult
  validateCustomizationRequests-->>CustomizationService: valid[], rejected[]
  loop depth-first per valid entry
    CustomizationService->>resolveCustomizationUrl: ValidatedCustomization → URL
    resolveCustomizationUrl-->>CustomizationService: absolute URL
    CustomizationService->>importFn: import(URL)
    importFn-->>CustomizationService: module
    CustomizationService->>CustomizationService: getUrlCustomizationModulePayload(module)
    CustomizationService->>CustomizationService: recurse requires[]
  end
  CustomizationService->>setCustomizations: apply loaded global payloads
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sedghi

Poem

🐰 A rabbit once typed ?customization=vet,
The overlay bloomed — no [object Object] yet!
With prefixes mapped and traversal denied,
Each module loads once, then stays applied.
formatValue trims the DICOM clutter away,
Now horses get name tags — hip hip, hooray! 🐴

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR has Context and Changes, but it is missing a Testing section and the checklist items/environment fields are still unchecked or blank. Add a Testing section with verification steps and complete the checklist by checking applicable items and filling OS, Node, and browser details.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add customization URL parameter' accurately describes the primary change—adding URL parameter support for loading custom modules without rebuild.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/customization-url-parameter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
tests/Customization.spec.ts (2)

6-8: ⚡ Quick win

Exercise the chaining entrypoint in this E2E path.

Using veterinaryOverlay skips validation of the veterinary -> requires -> veterinaryOverlay flow introduced with this sample module pair.

Suggested diff
   await visitStudyOptions(page, studyInstanceUID, {
-    customization: 'veterinaryOverlay',
+    customization: 'veterinary',
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Customization.spec.ts` around lines 6 - 8, The test is directly using
'veterinaryOverlay' as the customization parameter in the visitStudyOptions
call, which bypasses the chaining entrypoint validation. Change the
customization parameter from 'veterinaryOverlay' to 'veterinary' so that the
dependency chain flow (veterinary -> requires -> veterinaryOverlay) is properly
exercised and validated in this E2E test path.

16-21: ⚡ Quick win

Use auto-waiting text assertions to reduce E2E flake risk.

textContent() is a point-in-time read. Prefer locator text expectations so Playwright waits for final rendered content.

Suggested diff
-  const patientNameOverlayText = (await patientNameOverlayItem.textContent())?.trim() ?? '';
-  const patientNameValue = patientNameOverlayText.replace(/^Patient\s*/, '');
-
-  expect(patientNameValue.length).toBeGreaterThan(0);
-  expect(patientNameValue).not.toContain('[object Object]');
-  expect(patientNameValue).toMatch(/horse/i);
+  await expect(patientNameOverlayItem).not.toContainText('[object Object]');
+  await expect(patientNameOverlayItem).toContainText(/horse/i);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Customization.spec.ts` around lines 16 - 21, The test is using
textContent() which performs a point-in-time read and can cause flakiness in E2E
tests. Replace the textContent extraction approach with Playwright's
auto-waiting locator text expectations. Instead of extracting the text from
patientNameOverlayItem and performing assertions on the extracted value, use the
locator's built-in matchers like toHaveText() or toContainText() that
automatically wait for the final rendered content before asserting, eliminating
the need to manually extract and trim the text value.
tests/utils/visitStudy.ts (1)

3-8: ⚡ Quick win

Support multi-value customization params in test navigation helper.

visitStudyOptions currently only accepts a single customization value. Accepting string | string[] makes this helper reusable for multi-customization URL scenarios.

Suggested diff
 type VisitStudyOptions = {
   mode?: string;
   delay?: number;
   datasources?: string;
-  customization?: string;
+  customization?: string | string[];
 };
@@
   const params = new URLSearchParams({ StudyInstanceUIDs: studyInstanceUID });
-  if (customization) {
-    params.set('customization', customization);
-  }
+  const customizationValues = Array.isArray(customization)
+    ? customization
+    : customization
+      ? [customization]
+      : [];
+  for (const value of customizationValues) {
+    params.append('customization', value);
+  }

Also applies to: 31-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/visitStudy.ts` around lines 3 - 8, The `customization` property
in the `VisitStudyOptions` type currently only accepts a single string value,
but should support multiple customization parameters. Change the type definition
of the `customization` field from `string` to `string | string[]` in the
`VisitStudyOptions` type. Additionally, update the code that consumes this
parameter (referenced at lines 31-34) to handle both single string and array of
strings values appropriately when constructing URLs or passing them to the
navigation logic.
platform/app/src/utils/preserveQueryParameters.test.ts (1)

31-41: ⚡ Quick win

Add an overlap regression case for base + custom preserve keys.

This suite should also assert behavior when customization-provided keys include an existing base key and the current URL has values for that key (to prevent double-append regressions).

Suggested test addition
   it('uses customization service values for multi-key preservation', () => {
     const customizationService = {
       getValue: jest.fn().mockReturnValue(['customization', 'customizationAlt']),
     };
     const current = new URLSearchParams();
+    current.append('customization', 'a');
     current.append('customizationAlt', 'c');
     const out = new URLSearchParams();
     preserveQueryParameters(out, customizationService, current);
+    expect(out.getAll('customization')).toEqual(['a']);
     expect(out.getAll('customizationAlt')).toEqual(['c']);
     expect(customizationService.getValue).toHaveBeenCalled();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/app/src/utils/preserveQueryParameters.test.ts` around lines 31 - 41,
Add a new test case after the existing 'uses customization service values for
multi-key preservation' test to cover the overlap regression scenario. The test
should verify that when a query parameter key exists in both the base preserved
keys and the keys returned by the customization service's getValue method, and
the current URL contains a value for that overlapping key, the
preserveQueryParameters function appends that value only once to the output
URLSearchParams without duplication. Set up the test with a customization
service that returns keys including an existing base key, populate the current
URLSearchParams with a value for that overlapping key, call
preserveQueryParameters, and then assert that the output contains only a single
copy of that value.
platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts (1)

9-184: ⚡ Quick win

Add a regression test for same normalized request under different prefix mappings.

Current coverage doesn’t assert behavior when /default/A is requested across calls with different policy.prefixes.default. A focused test here would lock in correct cache semantics.

🧪 Suggested test case
+  it('does not reuse cache when the same normalized name resolves to a different base URL', async () => {
+    const importFn = jest.fn(async (url: string) => ({
+      customizations: { global: { marker: { value: url } } },
+    }));
+
+    await service.requires(['A'], {
+      policy: { prefixes: { default: './customizations-v1/' } },
+      importFn,
+    });
+    await service.requires(['A'], {
+      policy: { prefixes: { default: './customizations-v2/' } },
+      importFn,
+    });
+
+    expect(importFn).toHaveBeenCalledTimes(2);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts`
around lines 9 - 184, Add a new test case to verify cache semantics when the
same module name is requested with different policy prefix mappings. Create a
test that calls service.requires with a module like /default/A first with one
policy.prefixes.default configuration, then calls it again with a different
policy.prefixes.default value, and assert that the service correctly handles the
cache state (either reusing or reloading appropriately based on whether the
normalized paths are truly the same). This ensures the caching logic in the
requires method doesn't incorrectly share cached modules across different prefix
contexts.
platform/core/src/services/CustomizationService/validate.test.ts (1)

17-22: ⚡ Quick win

Add an interleaved mixed-case ordering regression test

Given the parser is case-insensitive and order-sensitive, add a case like Customization=a&customization=b&Customization=c to lock expected output order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/core/src/services/CustomizationService/validate.test.ts` around
lines 17 - 22, Add a new test case to the matches the parameter key
case-insensitively test block that creates a URLSearchParams with interleaved
mixed-case variations of the same parameter (such as Customization=a,
customization=b, Customization=c) and verify that parseCustomizationParams
returns them in the order they appear regardless of case differences. This
ensures the parser is truly order-sensitive while being case-insensitive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform/app/src/utils/preserveQueryParameters.ts`:
- Around line 25-32: The getPreserveKeys function returns an array by combining
preserveKeys and customKeys without deduplication, which can result in duplicate
keys being passed to preserveQueryParameters. Modify the return statement to
remove duplicates from the combined array before returning it, ensuring that if
a key exists in both preserveKeys and customKeys, it only appears once in the
final result.

In `@platform/core/src/services/CustomizationService/CustomizationService.ts`:
- Around line 370-373: The cache key for _urlCustomizationLoaded and
_urlCustomizationPending only uses request.normalized, which can cause different
modules to be incorrectly reused when different policy.prefixes are provided.
You need to modify the cache key generation to include policy information (such
as policy.prefixes) in addition to request.normalized to ensure each unique
combination of normalized name and policy prefix gets its own cache entry. Apply
this cache key change consistently across all locations where
_urlCustomizationLoaded or _urlCustomizationPending are accessed (the if
statement starting at line 370, the get call around lines 389-390, the
assignment around lines 407-413, and the pending check around lines 478-479).
- Around line 199-210: The catch block in applyWindowUrlCustomizations currently
logs all errors as warnings regardless of configuration, but when strict
customization policy is enabled, errors should cause the method to fail
(rethrow) rather than silently continue. Modify the catch block to check if
strict mode is enabled, and if so, rethrow the error after logging; otherwise,
keep the current warning behavior for non-strict mode.

In `@platform/core/src/services/CustomizationService/resolve.test.ts`:
- Around line 22-30: The beforeAll hook in this test suite mutates
window.location but never restores it after tests complete, which causes state
to leak into subsequent tests. Save the original window.location descriptor
before the beforeAll hook modifies it, then add an afterAll hook that uses
Object.defineProperty to restore window.location back to its original state
using the saved descriptor. This ensures the global state is cleaned up after
this test suite runs.

In `@platform/core/src/services/CustomizationService/validate.ts`:
- Around line 23-37: The current implementation using new Set(params.keys()) to
deduplicate keys loses the original ordering of customization values when keys
with different casing are interleaved in the query parameters. To fix this,
replace the Set-based deduplication approach with direct iteration through the
original params entries in their natural order. Instead of creating a
deduplicated set and then calling getAll for each unique key, iterate through
params' entries directly while still performing case-insensitive key matching,
ensuring all values are collected in the exact order they appear in the source
params object.

---

Nitpick comments:
In `@platform/app/src/utils/preserveQueryParameters.test.ts`:
- Around line 31-41: Add a new test case after the existing 'uses customization
service values for multi-key preservation' test to cover the overlap regression
scenario. The test should verify that when a query parameter key exists in both
the base preserved keys and the keys returned by the customization service's
getValue method, and the current URL contains a value for that overlapping key,
the preserveQueryParameters function appends that value only once to the output
URLSearchParams without duplication. Set up the test with a customization
service that returns keys including an existing base key, populate the current
URLSearchParams with a value for that overlapping key, call
preserveQueryParameters, and then assert that the output contains only a single
copy of that value.

In
`@platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts`:
- Around line 9-184: Add a new test case to verify cache semantics when the same
module name is requested with different policy prefix mappings. Create a test
that calls service.requires with a module like /default/A first with one
policy.prefixes.default configuration, then calls it again with a different
policy.prefixes.default value, and assert that the service correctly handles the
cache state (either reusing or reloading appropriately based on whether the
normalized paths are truly the same). This ensures the caching logic in the
requires method doesn't incorrectly share cached modules across different prefix
contexts.

In `@platform/core/src/services/CustomizationService/validate.test.ts`:
- Around line 17-22: Add a new test case to the matches the parameter key
case-insensitively test block that creates a URLSearchParams with interleaved
mixed-case variations of the same parameter (such as Customization=a,
customization=b, Customization=c) and verify that parseCustomizationParams
returns them in the order they appear regardless of case differences. This
ensures the parser is truly order-sensitive while being case-insensitive.

In `@tests/Customization.spec.ts`:
- Around line 6-8: The test is directly using 'veterinaryOverlay' as the
customization parameter in the visitStudyOptions call, which bypasses the
chaining entrypoint validation. Change the customization parameter from
'veterinaryOverlay' to 'veterinary' so that the dependency chain flow
(veterinary -> requires -> veterinaryOverlay) is properly exercised and
validated in this E2E test path.
- Around line 16-21: The test is using textContent() which performs a
point-in-time read and can cause flakiness in E2E tests. Replace the textContent
extraction approach with Playwright's auto-waiting locator text expectations.
Instead of extracting the text from patientNameOverlayItem and performing
assertions on the extracted value, use the locator's built-in matchers like
toHaveText() or toContainText() that automatically wait for the final rendered
content before asserting, eliminating the need to manually extract and trim the
text value.

In `@tests/utils/visitStudy.ts`:
- Around line 3-8: The `customization` property in the `VisitStudyOptions` type
currently only accepts a single string value, but should support multiple
customization parameters. Change the type definition of the `customization`
field from `string` to `string | string[]` in the `VisitStudyOptions` type.
Additionally, update the code that consumes this parameter (referenced at lines
31-34) to handle both single string and array of strings values appropriately
when constructing URLs or passing them to the navigation logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93677f12-e870-453e-83ef-ddd105467388

📥 Commits

Reviewing files that changed from the base of the PR and between 780d172 and 44a04ed.

📒 Files selected for processing (29)
  • extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx
  • extensions/default/src/ViewerLayout/ViewerHeader.tsx
  • extensions/default/src/customizations/overlayItemCustomization.tsx
  • platform/app/public/customizations/veterinary.js
  • platform/app/public/customizations/veterinaryOverlay.js
  • platform/app/src/App.tsx
  • platform/app/src/appInit.js
  • platform/app/src/utils/preserveQueryParameters.test.ts
  • platform/app/src/utils/preserveQueryParameters.ts
  • platform/core/src/services/CustomizationService/CustomizationService.init.test.ts
  • platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts
  • platform/core/src/services/CustomizationService/CustomizationService.ts
  • platform/core/src/services/CustomizationService/customizationUrl.ts
  • platform/core/src/services/CustomizationService/customizationUrlDefaults.ts
  • platform/core/src/services/CustomizationService/customizationUrlTypes.ts
  • platform/core/src/services/CustomizationService/getUrlCustomizationModulePayload.ts
  • platform/core/src/services/CustomizationService/resolve.test.ts
  • platform/core/src/services/CustomizationService/resolve.ts
  • platform/core/src/services/CustomizationService/validate.test.ts
  • platform/core/src/services/CustomizationService/validate.ts
  • platform/core/src/services/MultiMonitorService.ts
  • platform/core/src/services/index.ts
  • platform/core/src/utils/formatValue.js
  • platform/core/src/utils/index.ts
  • platform/docs/docs/platform/services/customization-service/customizationService.md
  • platform/docs/docs/platform/services/customization-service/specificCustomizations.md
  • tests/Customization.spec.ts
  • tests/utils/index.ts
  • tests/utils/visitStudy.ts

Comment thread platform/app/src/utils/preserveQueryParameters.ts
Comment thread platform/core/src/services/CustomizationService/resolve.test.ts
Comment thread platform/core/src/services/CustomizationService/validate.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform/core/src/services/CustomizationService/customizationUrlDefaults.ts`:
- Around line 11-13: Update the documentation comment in
customizationUrlDefaults.ts to reflect that the warn-and-skip behavior is only
applied in non-strict mode, and that strict mode (when policy.strict === true)
should throw on invalid customization query entries, failed imports, resolve
errors, and modules with no customization payload. Then locate the validation
and load call sites within the customizationUrlDefaults logic and implement
conditional behavior based on policy.strict: throw errors when strict mode is
enabled, and warn-and-skip when strict mode is disabled to restore the
configurable strict mode behavior that was originally supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2a4d5d1-9ce3-4874-92e4-78459d71d506

📥 Commits

Reviewing files that changed from the base of the PR and between 839d232 and e42e383.

📒 Files selected for processing (4)
  • platform/core/src/services/CustomizationService/CustomizationService.requires.test.ts
  • platform/core/src/services/CustomizationService/CustomizationService.ts
  • platform/core/src/services/CustomizationService/customizationUrlDefaults.ts
  • platform/docs/docs/platform/services/customization-service/specificCustomizations.md
✅ Files skipped from review due to trivial changes (1)
  • platform/docs/docs/platform/services/customization-service/specificCustomizations.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • platform/core/src/services/CustomizationService/CustomizationService.ts

Comment thread platform/core/src/services/CustomizationService/customizationUrlDefaults.ts Outdated
@wayfarer3130 wayfarer3130 requested a review from sedghi June 19, 2026 16:43
Comment thread platform/app/src/appInit.js Outdated
Comment thread platform/core/src/services/CustomizationService/CustomizationService.ts Outdated
Comment thread platform/core/src/services/MultiMonitorService.ts
@wayfarer3130

Copy link
Copy Markdown
Contributor Author

@sedghi - as requested, I've added additional examples, described in the testing block above, as well as in the documentation internally.

Comment thread platform/app/src/appInit.js Outdated
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.

3 participants