@hmcts/media-viewer 4.2.36 → 4.2.38-innerhtml-security-update

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/README.md CHANGED
@@ -245,18 +245,18 @@ Stop the local processes with `Ctrl+C`, then clean up Redis from the ICP worktre
245
245
  `docker compose -f docker-compose.yml down`.
246
246
 
247
247
  ### 5. Run Playwright tests
248
- Media Viewer is starting its Playwright migration with the same runner and
249
- reporting shape used in MC and MO, scaled to the current smoke coverage. The
250
- legacy Protractor and CodeceptJS functional packs still exist; new browser
251
- coverage should be added under `playwright_tests/`.
248
+ Media Viewer uses the Playwright runner and reporting shape used in MC and MO.
249
+ The CodeceptJS and Protractor/Cucumber runners are retired. New browser coverage
250
+ belongs under `playwright_tests/`.
252
251
 
253
252
  Current Playwright lanes:
254
253
 
255
254
  | Lane | Config/project | Command | Scope |
256
255
  | --- | --- | --- | --- |
257
256
  | Standalone smoke | `playwright.config.ts`, project `smoke` | `yarn test:playwright:smoke` or `yarn test:smoke` | One readiness contract: loads a standalone PDF and proves the rendered viewer, first page and canvas are usable. |
258
- | Migrated functional | `playwright.config.ts`, project `functional` | `yarn test:playwright:functional` | 74 fixture-backed browser contracts across 13 feature files, including separate failed PDF/image rendered-state diagnostics. Two additional image-annotation create contracts are discoverable, ticketed against [EXUI-5124](https://tools.hmcts.net/jira/browse/EXUI-5124), and excluded from the default selection because the current product does not persist an image draw-box annotation. See [`playwright_tests/functional/README.md`](playwright_tests/functional/README.md). |
257
+ | Migrated functional | `playwright.config.ts`, project `functional` | `yarn test:playwright:functional` | 84 default fixture-backed browser contracts across 13 feature files, with 86 discoverable when the two EXUI-5124 image-annotation contracts are explicitly included. Multimedia coverage is Chromium-only; external AAT/CCD contracts are separate diagnostics and are not migration assurance. See [`playwright_tests/functional/README.md`](playwright_tests/functional/README.md). |
259
258
  | External service diagnostics | `playwright.config.ts`, opt-in project `external-service-contracts` | `yarn test:playwright:external-service-contracts` | Optional live AAT CCD/DM Store/annotation probes for a deliberate environment investigation. The default command executes 6 non-defect service contracts; four CCD browser-route contracts tagged against [EXUI-5122](https://tools.hmcts.net/jira/browse/EXUI-5122) and [EXUI-5123](https://tools.hmcts.net/jira/browse/EXUI-5123) remain discoverable but are excluded by default. Use `PLAYWRIGHT_INCLUDE_KNOWN_DEFECTS=true` to discover and execute all 10. They are never part of normal PR assurance. |
259
+ | Cross-browser smoke | `playwright.config.ts`, projects `smoke-firefox` and `smoke-webkit` | `yarn test:crossbrowser` | Runs the same readiness contract in Firefox and WebKit and publishes separate JUnit/Odhín output under `functional-output/tests/playwright-crossbrowser`. |
260
260
  | Viewer support | `playwright.config.ts`, project `support` | `yarn test:playwright:support` | Proves the reusable PDF, image and unsupported-media fixtures, component objects and response diagnostics. |
261
261
 
262
262
  The current migration slice is deliberately separated from smoke: smoke proves
@@ -274,10 +274,10 @@ route mocks. Tests must not depend on execution order or share mutable
274
274
  documents; mutation-heavy AAT journeys must provision a document per test or
275
275
  reset it before reuse.
276
276
 
277
- Install Chromium once before local runs when the browser cache is empty:
277
+ Install the Playwright browsers once before local runs when the browser cache is empty:
278
278
 
279
279
  ```
280
- yarn test:setup:playwright-install-chromium
280
+ yarn test:setup:playwright-install-browsers
281
281
  ```
282
282
 
283
283
  Run the smoke project against a running standalone demo app. Start the app in
@@ -291,12 +291,16 @@ Then run the smoke in another terminal:
291
291
 
292
292
  ```
293
293
  yarn test:playwright:smoke
294
+ # unified Playwright accessibility pack (Axe, WAVE-like, screen-reader-like)
295
+ yarn test:a11y
296
+ # Firefox and WebKit smoke gate
297
+ yarn test:crossbrowser
294
298
  ```
295
299
 
296
300
  Override the smoke document and case id with `MV_SMOKE_PDF_DOCUMENT_URL` and
297
301
  `MV_SMOKE_CASE_ID`. `yarn test:smoke` and `yarn test:local:aat` run the
298
302
  Playwright smoke, so the standalone and local-AAT PDF loading journeys no longer
299
- fall back to CodeceptJS.
303
+ use only the Playwright projects.
300
304
 
301
305
  The lane wrapper commands write Playwright evidence under `functional-output/tests`:
302
306
 
@@ -305,6 +309,8 @@ The lane wrapper commands write Playwright evidence under `functional-output/tes
305
309
  | Viewer support | `functional-output/tests/playwright-support/odhin-report/xui-playwright-support.html` | `functional-output/tests/playwright-support/playwright-support-junit.xml` | `functional-output/tests/playwright-support/test-results` |
306
310
  | Smoke | `functional-output/tests/playwright-smoke/odhin-report/xui-playwright-smoke.html` | `functional-output/tests/playwright-smoke/playwright-smoke-junit.xml` | `functional-output/tests/playwright-smoke/test-results` |
307
311
  | Migrated functional | `functional-output/tests/playwright-functional/odhin-report/xui-playwright-functional.html` | `functional-output/tests/playwright-functional/playwright-functional-junit.xml` | `functional-output/tests/playwright-functional/test-results` |
312
+ | Accessibility | `functional-output/tests/playwright-accessibility/odhin-report/xui-playwright-accessibility.html` | `functional-output/tests/playwright-accessibility/playwright-accessibility-junit.xml` | `functional-output/tests/playwright-accessibility/test-results` |
313
+ | Cross-browser smoke | `functional-output/tests/playwright-crossbrowser/odhin-report/xui-playwright-crossbrowser.html` | `functional-output/tests/playwright-crossbrowser/playwright-crossbrowser-junit.xml` | `functional-output/tests/playwright-crossbrowser/test-results` |
308
314
 
309
315
  Those are the default lane-specific paths. CNP keeps preview and AAT viewer
310
316
  support evidence separate under `functional-output/tests/playwright-support/preview`
@@ -336,9 +342,10 @@ Reporting behavior follows the MC/MO pattern:
336
342
  HTML Publisher links.
337
343
 
338
344
  The Jenkins `YarnBuilder` performs its immutable dependency install before the
339
- first setup task. The pipeline then installs Puppeteer Chrome once for legacy
340
- tests and Chromium into the workspace-local `PLAYWRIGHT_BROWSERS_PATH`, and sets
341
- `PLAYWRIGHT_SKIP_INSTALL=true` so Playwright lanes do not reinstall it.
345
+ first setup task. The pipeline installs Puppeteer Chrome for Karma and the
346
+ functional preflight, then installs Chromium into the workspace-local
347
+ `PLAYWRIGHT_BROWSERS_PATH` and sets `PLAYWRIGHT_SKIP_INSTALL=true` so Playwright
348
+ lanes do not reinstall it.
342
349
 
343
350
  Useful overrides:
344
351
  - `PLAYWRIGHT_BASE_URL` or `TEST_URL`: target application URL, default `http://localhost:3000/`
@@ -384,10 +391,6 @@ Migration boundaries:
384
391
  - Put new native Playwright specs under `playwright_tests/`.
385
392
  - Keep screen interactions and reusable locators in page objects under
386
393
  `playwright_tests/pages/`; keep assertions visible in specs.
387
- - Historical CodeceptJS scenarios are retained as source traceability only;
388
- their executable pipeline routing is retired once the mapped Playwright
389
- contract is selected by default or is represented by a discoverable,
390
- ticketed product-defect contract.
391
394
  - Add stable report output paths for every new Playwright lane so Jenkins can
392
395
  publish Odhín and JUnit and archive failure diagnostics without bespoke stage
393
396
  logic.
@@ -607,7 +610,9 @@ The list of exceptions thrown by the Media Viewer are as follows:
607
610
  - HttpErrorResponse
608
611
  - PasswordException
609
612
 
610
- ## Cucumber Tests
611
- ```
612
- npm run package, npm run e2e:cucumber
613
- ```
613
+ ## Legacy browser tests
614
+
615
+ The CodeceptJS and Protractor/Cucumber runners were retired as part of the
616
+ Playwright migration. Migration closure was recorded in PR #82; the active
617
+ browser test source is now Playwright. Use the Playwright commands above for
618
+ supported browser tests. No recurring legacy-parity test is retained.
@@ -3353,26 +3353,55 @@ class TextHighlightDirective {
3353
3353
  highlightInputText(textToHighlight) {
3354
3354
  this.resetHighlight();
3355
3355
  this.textToHighlight = textToHighlight;
3356
- const searchPattern = new RegExp(textToHighlight, 'gi');
3356
+ const searchPattern = new RegExp(this.escapeRegExp(textToHighlight), 'gi');
3357
3357
  const hostElement = this.element.nativeElement;
3358
- if (hostElement.innerHTML.match(searchPattern)) {
3359
- hostElement.innerHTML = hostElement.innerHTML
3360
- .replace(searchPattern, this.highlightPattern('$&'));
3361
- }
3358
+ this.highlightTextNodes(hostElement, searchPattern);
3362
3359
  this.textToHighlight = undefined;
3363
3360
  }
3364
3361
  resetHighlight() {
3365
3362
  const hostElement = this.element.nativeElement;
3366
- const searchPattern = new RegExp(this.highlightPattern('(.*?)'), 'gi');
3367
- while (hostElement.innerHTML.match(searchPattern)) {
3368
- const matchGroups = searchPattern.exec(hostElement.innerHTML);
3369
- if (matchGroups) {
3370
- hostElement.innerHTML = hostElement.innerHTML.replace(matchGroups[0], matchGroups[1]);
3363
+ Array.from(hostElement.querySelectorAll('span.mvTextHighlight')).forEach((highlightElement) => {
3364
+ highlightElement.replaceWith(document.createTextNode(highlightElement.textContent || ''));
3365
+ });
3366
+ hostElement.normalize();
3367
+ }
3368
+ highlightTextNodes(hostElement, searchPattern) {
3369
+ const walker = document.createTreeWalker(hostElement, NodeFilter.SHOW_TEXT);
3370
+ const textNodes = [];
3371
+ let currentNode = walker.nextNode();
3372
+ while (currentNode) {
3373
+ textNodes.push(currentNode);
3374
+ currentNode = walker.nextNode();
3375
+ }
3376
+ textNodes.forEach((textNode) => this.highlightTextNode(textNode, searchPattern));
3377
+ }
3378
+ highlightTextNode(textNode, searchPattern) {
3379
+ const text = textNode.textContent || '';
3380
+ const matches = Array.from(text.matchAll(searchPattern));
3381
+ if (matches.length === 0) {
3382
+ return;
3383
+ }
3384
+ const fragment = document.createDocumentFragment();
3385
+ let currentIndex = 0;
3386
+ matches.forEach((match) => {
3387
+ const matchIndex = match.index || 0;
3388
+ const matchText = match[0];
3389
+ if (matchIndex > currentIndex) {
3390
+ fragment.appendChild(document.createTextNode(text.slice(currentIndex, matchIndex)));
3371
3391
  }
3392
+ const highlightElement = document.createElement('span');
3393
+ highlightElement.className = 'mvTextHighlight';
3394
+ highlightElement.textContent = matchText;
3395
+ fragment.appendChild(highlightElement);
3396
+ currentIndex = matchIndex + matchText.length;
3397
+ });
3398
+ if (currentIndex < text.length) {
3399
+ fragment.appendChild(document.createTextNode(text.slice(currentIndex)));
3372
3400
  }
3401
+ textNode.replaceWith(fragment);
3373
3402
  }
3374
- highlightPattern(dynamicText) {
3375
- return '<span class="mvTextHighlight">' + dynamicText + '</span>';
3403
+ escapeRegExp(value) {
3404
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3376
3405
  }
3377
3406
  /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: TextHighlightDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
3378
3407
  /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.26", type: TextHighlightDirective, isStandalone: false, selector: "[mvTextHighlight]", inputs: { textToHighlight: "textToHighlight" }, ngImport: i0 }); }
@@ -7943,12 +7972,14 @@ class GovUkLabelComponent {
7943
7972
  /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.26", ngImport: i0, type: GovUkLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7944
7973
  /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.26", type: GovUkLabelComponent, isStandalone: false, selector: "mv-gov-label", inputs: { config: "config" }, ngImport: i0, template: `<h1 *ngIf="config.isPageHeading else noHeading">
7945
7974
  <label *ngIf="config.label" [class]="config.classes + ' govuk-label'"
7946
- [for]="config.id" [innerHTML]="config.label">
7975
+ [for]="config.id">
7976
+ {{ config.label }}
7947
7977
  </label>
7948
7978
  </h1>
7949
7979
  <ng-template #noHeading>
7950
7980
  <label *ngIf="config.label" [class]="config.classes + ' govuk-label'"
7951
- [for]="config.id" [innerHTML]="config.label">
7981
+ [for]="config.id">
7982
+ {{ config.label }}
7952
7983
  </label>
7953
7984
  </ng-template>
7954
7985
  `, isInline: true, dependencies: [{ kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
@@ -7959,12 +7990,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.26", ngImpo
7959
7990
  selector: 'mv-gov-label',
7960
7991
  template: `<h1 *ngIf="config.isPageHeading else noHeading">
7961
7992
  <label *ngIf="config.label" [class]="config.classes + ' govuk-label'"
7962
- [for]="config.id" [innerHTML]="config.label">
7993
+ [for]="config.id">
7994
+ {{ config.label }}
7963
7995
  </label>
7964
7996
  </h1>
7965
7997
  <ng-template #noHeading>
7966
7998
  <label *ngIf="config.label" [class]="config.classes + ' govuk-label'"
7967
- [for]="config.id" [innerHTML]="config.label">
7999
+ [for]="config.id">
8000
+ {{ config.label }}
7968
8001
  </label>
7969
8002
  </ng-template>
7970
8003
  `,