From a36dde57dcb20636db3731e05a9329c449567c30 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 12 Jun 2026 11:50:29 +0200 Subject: [PATCH 1/8] fix: update PDFViewer to use dynamic origin for resource URLs --- .../document-viewer-web/src/components/PDFViewer.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx index 1f3b7da0a6..578833ad28 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx @@ -7,9 +7,11 @@ import { useZoomScale } from "../utils/useZoomScale"; import BaseViewer from "./BaseViewer"; import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer"; import { If } from "@mendix/widget-plugin-component-kit/If"; + +const origin = window.location.origin; const options = { - cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/", - standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts" + cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, + standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` }; const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => { From df51a3dfe16c14e7ac2e8e9db1ce108ed79925a5 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 12 Jun 2026 14:01:14 +0200 Subject: [PATCH 2/8] docs(document-viewer): update changelog with standard font fix Co-Authored-By: Claude Sonnet 4.6 --- packages/pluggableWidgets/document-viewer-web/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md b/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md index b2800d5a62..4af9c0d459 100644 --- a/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md +++ b/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We changed the internal structure of the widget +### Fixed + +- We fixed an issue where PDF standard fonts (e.g. ZapfDingbats) failed to load when the PDF.js worker was served from a cross-origin URL, causing glyphs such as checkmarks to render as blank rectangles. + ## [1.2.0] - 2025-10-29 ### Added From 802d15566ef1d3200d63b1b320af095af69efcb8 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 12 Jun 2026 14:01:24 +0200 Subject: [PATCH 3/8] chore(openspec): archive document-viewer-render-forms-fix change Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/document-viewer-checkbox-bug.md | 63 +++++++++++++++++++ .../.openspec.yaml | 2 + .../design.md | 34 ++++++++++ .../proposal.md | 25 ++++++++ .../specs/pdf-form-rendering/spec.md | 21 +++++++ .../tasks.md | 10 +++ 6 files changed, 155 insertions(+) create mode 100644 docs/plans/document-viewer-checkbox-bug.md create mode 100644 openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/.openspec.yaml create mode 100644 openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md create mode 100644 openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md create mode 100644 openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md create mode 100644 openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md diff --git a/docs/plans/document-viewer-checkbox-bug.md b/docs/plans/document-viewer-checkbox-bug.md new file mode 100644 index 0000000000..39580700f2 --- /dev/null +++ b/docs/plans/document-viewer-checkbox-bug.md @@ -0,0 +1,63 @@ +# Bug: PDF Form Checkboxes Not Displaying Checked State + +**Widget:** Document Viewer v1.2.0 +**Mendix version:** 10.24.9 +**Status:** Fixed + +--- + +## Symptom + +A W9 PDF generated via .NET has a checked checkbox in Section 3.a ("C corporation"). When opened directly in a browser (Chrome, Firefox native viewer), the checkbox renders correctly as checked. When displayed in the Document Viewer widget, the checkbox appears unchecked. + +--- + +## Root Cause + +The checkmark is drawn by a PDF Form XObject using the `/ZaDb` (ZapfDingbats) standard font with glyph `0x34`. PDF.js substitutes standard fonts with bundled Foxit equivalents, fetching them from `standardFontDataUrl`. + +`PDFViewer.tsx` configured this as a relative URL: + +```ts +const options = { + cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/", + standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts/" +}; +``` + +The PDF.js worker is loaded from `//unpkg.com/pdfjs-dist@.../pdf.worker.min.mjs` — a cross-origin URL. A worker loaded from a different origin has no document base URL, so `fetch()` cannot resolve relative paths. The worker throws: + +``` +TypeError: Failed to execute 'fetch' on 'WorkerGlobalScope': +Failed to parse URL from /widgets/com/mendix/shared/pdfjs/standard_fonts/FoxitDingbats.pfb +``` + +Font load silently fails → ZapfDingbats not available → checkmark glyph renders as blank rectangle. + +The browser's native PDF viewer is unaffected because it handles font resolution internally without a web worker. + +--- + +## Fix + +**File:** `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` + +```diff ++const origin = window.location.origin; + const options = { +- cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/", +- standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts/" ++ cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, ++ standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` + }; +``` + +`window.location.origin` is the Mendix app origin (e.g. `https://myapp.mendixcloud.com`). The worker can fetch absolute URLs regardless of where it was loaded from. + +--- + +## Verification + +1. Load customer W9 PDF — Section 3.a "C corporation" checkbox shows as checked ✓ +2. Build: `pnpm --filter @mendix/document-viewer-web run build` +3. Network tab shows absolute URL `http:///widgets/.../FoxitDingbats.pfb` with 200 response (or request goes to worker — confirmed via console, no more `loadFont` warning) diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/.openspec.yaml b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/.openspec.yaml new file mode 100644 index 0000000000..e0c0898ffd --- /dev/null +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-11 diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md new file mode 100644 index 0000000000..5527122b5d --- /dev/null +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md @@ -0,0 +1,34 @@ +## Context + +Document Viewer v1.2.0 uses `react-pdf` to render PDFs via PDF.js. The `options` object passed to the `` component includes `cMapUrl` and `standardFontDataUrl` as relative paths. PDF.js passes these to the worker thread for font/cmap fetching. When the worker is loaded from a cross-origin URL (the default is unpkg CDN: `//unpkg.com/pdfjs-dist@.../pdf.worker.min.mjs`), the worker's `fetch()` cannot resolve relative URLs — it has no document origin to resolve against — causing a `TypeError: Failed to parse URL` and silent font load failure. + +The customer's W9 PDF draws a checkmark using the ZapfDingbats standard font (glyph `0x34`) via a Form XObject. Without the font, PDF.js renders a blank rectangle. + +## Goals / Non-Goals + +**Goals:** + +- Fix standard font and cmap fetching when the PDF.js worker is cross-origin +- No new XML properties, no API surface changes, no dependency updates + +**Non-Goals:** + +- Changing how the worker URL is configured +- Supporting self-hosted worker deployments (already supported via `pdfjsWorkerUrl` prop) + +## Decisions + +### Use `window.location.origin` to make resource URLs absolute + +Prepend `window.location.origin` to both `cMapUrl` and `standardFontDataUrl` at module evaluation time. + +**Rationale:** The worker needs absolute URLs. `window.location.origin` is always the Mendix app origin — correct for all deployment environments. Evaluated at module load (not per-render), so no React re-render cost. + +**Alternative considered:** Move `options` inside the component and use `useMemo`. Rejected — no reactive dependencies, module-scope evaluation is simpler and equivalent. + +**Alternative considered:** Set `useWorkerFetch: false` to force main-thread font loading. Rejected — works around the symptom, not the cause; disabling worker fetch has broader performance implications. + +## Risks / Trade-offs + +- `window.location.origin` is not available in SSR/test environments. Tests currently stub this or don't exercise PDF rendering — no impact. If server-side rendering is ever added, this will need to be guarded. +- If the Mendix app is served from a subpath (e.g. `/app/`), `origin` alone is correct — fonts live under `/widgets/`, not the subpath. diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md new file mode 100644 index 0000000000..6ff3801568 --- /dev/null +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md @@ -0,0 +1,25 @@ +## Why + +PDFs containing glyphs from ZapfDingbats (a PDF standard font) — such as checkmarks generated by .NET PDF libraries — render blank in Document Viewer. The checkmark is drawn via a Form XObject using the ZapfDingbats font, which PDF.js must fetch from `standardFontDataUrl`. The URL was relative, causing the PDF.js worker (loaded from unpkg CDN, a different origin) to fail parsing it as an absolute URL. + +## What Changes + +- `standardFontDataUrl` and `cMapUrl` in `PDFViewer.tsx` are prefixed with `window.location.origin` to produce absolute URLs, allowing the worker to fetch font resources regardless of where it was loaded from. + +## Capabilities + +### New Capabilities + +- `pdf-form-rendering`: Correct visual rendering of standard-font glyphs (ZapfDingbats checkmarks, Symbol characters) in PDFs rendered by the Document Viewer widget + +### Modified Capabilities + + + +## Impact + +- **File**: `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` +- **Behavior**: ZapfDingbats and other standard font glyphs now render correctly when PDF.js worker is loaded from a cross-origin URL (e.g. unpkg CDN) +- **No API or XML changes** +- **No dependency version changes** +- **Affected widget**: `@mendix/document-viewer-web` v1.2.0+ diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md new file mode 100644 index 0000000000..5c49bc37b5 --- /dev/null +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: Standard font glyphs render correctly in PDFs + +The Document Viewer SHALL correctly render PDF glyphs that depend on PDF standard fonts (ZapfDingbats, Symbol, etc.) by fetching font resources using absolute URLs resolvable by the PDF.js worker. + +#### Scenario: ZapfDingbats checkmark renders in cross-origin worker context + +- **WHEN** a PDF contains a glyph drawn from the ZapfDingbats standard font (e.g. a checkmark drawn via a Form XObject) +- **AND** the PDF.js worker is loaded from a cross-origin URL (e.g. unpkg CDN) +- **THEN** the glyph SHALL render visibly on the canvas + +#### Scenario: Font fetch uses absolute URL + +- **WHEN** PDF.js requests a standard font file from the worker thread +- **THEN** the request URL SHALL be an absolute URL including the application origin (e.g. `https://example.com/widgets/.../FoxitDingbats.pfb`) + +#### Scenario: PDFs without standard fonts are unaffected + +- **WHEN** a PDF contains no glyphs requiring standard font substitution +- **THEN** rendering SHALL be identical to previous behavior diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md new file mode 100644 index 0000000000..5696490d0c --- /dev/null +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md @@ -0,0 +1,10 @@ +## 1. Implementation + +- [x] 1.1 Prepend `window.location.origin` to `cMapUrl` and `standardFontDataUrl` in `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` + +## 2. Verification + +- [x] 2.1 Run unit tests: `cd packages/pluggableWidgets/document-viewer-web && pnpm run test` +- [x] 2.2 Build widget: `pnpm --filter @mendix/document-viewer-web run build` +- [x] 2.3 Verify customer W9 PDF shows Section 3.a checkbox as checked in Document Viewer +- [x] 2.4 Verify a PDF without AcroForms renders correctly (no regression) From b412da633ad286ff428c36429c26d915e0d26f53 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 16 Jun 2026 10:45:09 +0200 Subject: [PATCH 4/8] fix: update PDFViewer to use dynamic origin for resource URLs Use mx.appUrl (with window.location.origin fallback) for cMapUrl and standardFontDataUrl so the PDF.js worker can resolve font and cmap resources when loaded from a cross-origin URL. Co-Authored-By: Claude Sonnet 4.6 --- packages/pluggableWidgets/document-viewer-web/CHANGELOG.md | 2 +- .../document-viewer-web/src/components/PDFViewer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md b/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md index 4af9c0d459..712e4c27b2 100644 --- a/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md +++ b/packages/pluggableWidgets/document-viewer-web/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed -- We fixed an issue where PDF standard fonts (e.g. ZapfDingbats) failed to load when the PDF.js worker was served from a cross-origin URL, causing glyphs such as checkmarks to render as blank rectangles. +- We fixed an issue where checkmarks and other special characters in PDFs were not displayed correctly. ## [1.2.0] - 2025-10-29 diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx index 578833ad28..7677bdc97f 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx @@ -8,7 +8,7 @@ import BaseViewer from "./BaseViewer"; import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer"; import { If } from "@mendix/widget-plugin-component-kit/If"; -const origin = window.location.origin; +const origin: string = (window as any).mx?.appUrl ?? window.location.origin; const options = { cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` From 93ca60fbee70a4ec34825a378da64cf43e76b8a7 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 16 Jun 2026 11:08:48 +0200 Subject: [PATCH 5/8] fix: strip trailing slash from mx.appUrl and add Window type declaration Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/document-viewer-checkbox-bug.md | 63 ------------------- .../src/components/PDFViewer.tsx | 2 +- .../document-viewer-web/typings/global.d.ts | 7 +++ 3 files changed, 8 insertions(+), 64 deletions(-) delete mode 100644 docs/plans/document-viewer-checkbox-bug.md create mode 100644 packages/pluggableWidgets/document-viewer-web/typings/global.d.ts diff --git a/docs/plans/document-viewer-checkbox-bug.md b/docs/plans/document-viewer-checkbox-bug.md deleted file mode 100644 index 39580700f2..0000000000 --- a/docs/plans/document-viewer-checkbox-bug.md +++ /dev/null @@ -1,63 +0,0 @@ -# Bug: PDF Form Checkboxes Not Displaying Checked State - -**Widget:** Document Viewer v1.2.0 -**Mendix version:** 10.24.9 -**Status:** Fixed - ---- - -## Symptom - -A W9 PDF generated via .NET has a checked checkbox in Section 3.a ("C corporation"). When opened directly in a browser (Chrome, Firefox native viewer), the checkbox renders correctly as checked. When displayed in the Document Viewer widget, the checkbox appears unchecked. - ---- - -## Root Cause - -The checkmark is drawn by a PDF Form XObject using the `/ZaDb` (ZapfDingbats) standard font with glyph `0x34`. PDF.js substitutes standard fonts with bundled Foxit equivalents, fetching them from `standardFontDataUrl`. - -`PDFViewer.tsx` configured this as a relative URL: - -```ts -const options = { - cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/", - standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts/" -}; -``` - -The PDF.js worker is loaded from `//unpkg.com/pdfjs-dist@.../pdf.worker.min.mjs` — a cross-origin URL. A worker loaded from a different origin has no document base URL, so `fetch()` cannot resolve relative paths. The worker throws: - -``` -TypeError: Failed to execute 'fetch' on 'WorkerGlobalScope': -Failed to parse URL from /widgets/com/mendix/shared/pdfjs/standard_fonts/FoxitDingbats.pfb -``` - -Font load silently fails → ZapfDingbats not available → checkmark glyph renders as blank rectangle. - -The browser's native PDF viewer is unaffected because it handles font resolution internally without a web worker. - ---- - -## Fix - -**File:** `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` - -```diff -+const origin = window.location.origin; - const options = { -- cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/", -- standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts/" -+ cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, -+ standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` - }; -``` - -`window.location.origin` is the Mendix app origin (e.g. `https://myapp.mendixcloud.com`). The worker can fetch absolute URLs regardless of where it was loaded from. - ---- - -## Verification - -1. Load customer W9 PDF — Section 3.a "C corporation" checkbox shows as checked ✓ -2. Build: `pnpm --filter @mendix/document-viewer-web run build` -3. Network tab shows absolute URL `http:///widgets/.../FoxitDingbats.pfb` with 200 response (or request goes to worker — confirmed via console, no more `loadFont` warning) diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx index 7677bdc97f..a492de0f76 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx @@ -8,7 +8,7 @@ import BaseViewer from "./BaseViewer"; import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer"; import { If } from "@mendix/widget-plugin-component-kit/If"; -const origin: string = (window as any).mx?.appUrl ?? window.location.origin; +const origin: string = (window.mx?.appUrl ?? window.location.origin).replace(/\/$/, ""); const options = { cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` diff --git a/packages/pluggableWidgets/document-viewer-web/typings/global.d.ts b/packages/pluggableWidgets/document-viewer-web/typings/global.d.ts new file mode 100644 index 0000000000..3c83dddd5d --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/typings/global.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + mx?: { appUrl?: string }; + } +} + +export {}; From fa9b1346bfde1cb12bc07c0394a1d63a8d6f8530 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 16 Jun 2026 15:27:56 +0200 Subject: [PATCH 6/8] fix: reorganize imports in PDFViewer and add CSS module declaration --- .../document-viewer-web/src/components/PDFViewer.tsx | 6 +++--- .../document-viewer-web/typings/modules.d.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx index a492de0f76..e7b93313a2 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx @@ -1,12 +1,12 @@ import { ChangeEvent, FormEvent, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useState } from "react"; import { Document, Page, pdfjs } from "react-pdf"; +import { If } from "@mendix/widget-plugin-component-kit/If"; import "react-pdf/dist/Page/AnnotationLayer.css"; import "react-pdf/dist/Page/TextLayer.css"; -import { downloadFile } from "../utils/helpers"; -import { useZoomScale } from "../utils/useZoomScale"; import BaseViewer from "./BaseViewer"; import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer"; -import { If } from "@mendix/widget-plugin-component-kit/If"; +import { downloadFile } from "../utils/helpers"; +import { useZoomScale } from "../utils/useZoomScale"; const origin: string = (window.mx?.appUrl ?? window.location.origin).replace(/\/$/, ""); const options = { diff --git a/packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts b/packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts new file mode 100644 index 0000000000..cbe652dbe0 --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts @@ -0,0 +1 @@ +declare module "*.css"; From bd769e1d3e1658854bfcc50f264f5325cf3a9f0b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 16 Jun 2026 15:46:02 +0200 Subject: [PATCH 7/8] fix: use absolute origin for resource URLs in PDFViewer and add supporting typings --- .../design.md | 20 ++++++++++++++----- .../proposal.md | 10 ++++++++-- .../specs/pdf-form-rendering/spec.md | 2 +- .../tasks.md | 3 ++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md index 5527122b5d..59251490e0 100644 --- a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/design.md @@ -18,17 +18,27 @@ The customer's W9 PDF draws a checkmark using the ZapfDingbats standard font (gl ## Decisions -### Use `window.location.origin` to make resource URLs absolute +### Use an absolute `origin` to make resource URLs absolute -Prepend `window.location.origin` to both `cMapUrl` and `standardFontDataUrl` at module evaluation time. +Compute a module-scope `origin` and prepend it to both `cMapUrl` and `standardFontDataUrl` at module evaluation time: -**Rationale:** The worker needs absolute URLs. `window.location.origin` is always the Mendix app origin — correct for all deployment environments. Evaluated at module load (not per-render), so no React re-render cost. +```ts +const origin: string = (window.mx?.appUrl ?? window.location.origin).replace(/\/$/, ""); +``` + +**Rationale:** The worker needs absolute URLs. `window.mx.appUrl` is the canonical Mendix application URL and is correct even when the app is served from a non-origin base path or behind a reverse proxy; `window.location.origin` is the fallback when `mx.appUrl` is unset (e.g. tests). The trailing-slash strip prevents `//` in the joined resource URLs. Evaluated at module load (not per-render), so no React re-render cost. + +**Alternative considered:** Use `window.location.origin` alone. Rejected — does not account for apps served under a base path that `mx.appUrl` encodes. **Alternative considered:** Move `options` inside the component and use `useMemo`. Rejected — no reactive dependencies, module-scope evaluation is simpler and equivalent. **Alternative considered:** Set `useWorkerFetch: false` to force main-thread font loading. Rejected — works around the symptom, not the cause; disabling worker fetch has broader performance implications. +### Supporting typings + +`window.mx` is not in the widget's ambient types, and CSS imports (`react-pdf/dist/Page/*.css`) need a module declaration. Added `typings/global.d.ts` (declares `Window.mx?: { appUrl?: string }`) and `typings/modules.d.ts` (`declare module "*.css"`). + ## Risks / Trade-offs -- `window.location.origin` is not available in SSR/test environments. Tests currently stub this or don't exercise PDF rendering — no impact. If server-side rendering is ever added, this will need to be guarded. -- If the Mendix app is served from a subpath (e.g. `/app/`), `origin` alone is correct — fonts live under `/widgets/`, not the subpath. +- Neither `window.mx?.appUrl` nor `window.location.origin` is guaranteed in SSR. Tests don't exercise PDF rendering and `mx.appUrl` falls back to `location.origin` — no impact. If server-side rendering is ever added, this will need to be guarded. +- If the Mendix app is served from a subpath (e.g. `/app/`), `mx.appUrl` already encodes it correctly; fonts resolve under the app's `/widgets/` path. diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md index 6ff3801568..0eb7d1377b 100644 --- a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/proposal.md @@ -4,7 +4,10 @@ PDFs containing glyphs from ZapfDingbats (a PDF standard font) — such as check ## What Changes -- `standardFontDataUrl` and `cMapUrl` in `PDFViewer.tsx` are prefixed with `window.location.origin` to produce absolute URLs, allowing the worker to fetch font resources regardless of where it was loaded from. +- `standardFontDataUrl` and `cMapUrl` in `PDFViewer.tsx` are prefixed with an absolute `origin` so the worker can fetch font resources regardless of where it was loaded from. +- `origin` resolves to `window.mx.appUrl` (the Mendix app URL) when available, falling back to `window.location.origin`. A trailing slash is stripped to avoid double slashes in the resulting resource URLs. +- A trailing slash is added to `standard_fonts/` so the font directory URL is well-formed. +- Supporting typings added: `window.mx` declaration (`typings/global.d.ts`) and a CSS module declaration (`typings/modules.d.ts`). ## Capabilities @@ -18,7 +21,10 @@ PDFs containing glyphs from ZapfDingbats (a PDF standard font) — such as check ## Impact -- **File**: `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` +- **Files**: + - `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` + - `packages/pluggableWidgets/document-viewer-web/typings/global.d.ts` (new — `window.mx` type) + - `packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts` (new — `*.css` module type) - **Behavior**: ZapfDingbats and other standard font glyphs now render correctly when PDF.js worker is loaded from a cross-origin URL (e.g. unpkg CDN) - **No API or XML changes** - **No dependency version changes** diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md index 5c49bc37b5..13c77430d3 100644 --- a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/specs/pdf-form-rendering/spec.md @@ -13,7 +13,7 @@ The Document Viewer SHALL correctly render PDF glyphs that depend on PDF standar #### Scenario: Font fetch uses absolute URL - **WHEN** PDF.js requests a standard font file from the worker thread -- **THEN** the request URL SHALL be an absolute URL including the application origin (e.g. `https://example.com/widgets/.../FoxitDingbats.pfb`) +- **THEN** the request URL SHALL be an absolute URL rooted at the Mendix application URL (`window.mx.appUrl`, falling back to `window.location.origin`, with any trailing slash stripped) — e.g. `https://example.com/widgets/.../FoxitDingbats.pfb` #### Scenario: PDFs without standard fonts are unaffected diff --git a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md index 5696490d0c..383f0392a8 100644 --- a/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md +++ b/openspec/changes/archive/2026-06-12-document-viewer-render-forms-fix/tasks.md @@ -1,6 +1,7 @@ ## 1. Implementation -- [x] 1.1 Prepend `window.location.origin` to `cMapUrl` and `standardFontDataUrl` in `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` +- [x] 1.1 Prepend an absolute `origin` (`window.mx?.appUrl ?? window.location.origin`, trailing slash stripped) to `cMapUrl` and `standardFontDataUrl` in `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx` +- [x] 1.2 Add `typings/global.d.ts` (`window.mx`) and `typings/modules.d.ts` (`*.css`) ## 2. Verification From 92c2cfa99014ddd0a40fb781e621ddb5a9ff6c7f Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Thu, 18 Jun 2026 16:23:11 +0200 Subject: [PATCH 8/8] test: add unit tests --- .../document-viewer-web/package.json | 2 +- .../components/__tests__/BaseViewer.spec.tsx | 65 ++++ .../components/__tests__/PDFViewer.spec.tsx | 302 ++++++++++++++++++ .../src/utils/__tests__/dimension.spec.ts | 71 ++++ .../src/utils/__tests__/helpers.spec.ts | 36 +++ .../src/utils/__tests__/useZoomScale.spec.ts | 59 ++++ 6 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 packages/pluggableWidgets/document-viewer-web/src/components/__tests__/BaseViewer.spec.tsx create mode 100644 packages/pluggableWidgets/document-viewer-web/src/components/__tests__/PDFViewer.spec.tsx create mode 100644 packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/dimension.spec.ts create mode 100644 packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/helpers.spec.ts create mode 100644 packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/useZoomScale.spec.ts diff --git a/packages/pluggableWidgets/document-viewer-web/package.json b/packages/pluggableWidgets/document-viewer-web/package.json index 4d914e44cf..cee7a9e7c0 100644 --- a/packages/pluggableWidgets/document-viewer-web/package.json +++ b/packages/pluggableWidgets/document-viewer-web/package.json @@ -39,7 +39,7 @@ "publish-marketplace": "rui-publish-marketplace", "release": "pluggable-widgets-tools release:web", "start": "pluggable-widgets-tools start:server", - "test": "echo 'FIXME: Add unit tests'", + "test": "pluggable-widgets-tools test:unit:web", "update-changelog": "rui-update-changelog-widget", "verify": "rui-verify-package-format" }, diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/BaseViewer.spec.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/BaseViewer.spec.tsx new file mode 100644 index 0000000000..4b47656e45 --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/BaseViewer.spec.tsx @@ -0,0 +1,65 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import BaseViewer, { BaseControlViewer } from "../BaseViewer"; + +const file = { + status: "available" as const, + value: { uri: "https://apps.example.com/file/42.pdf", name: "report.pdf" } +}; + +describe("BaseViewer", () => { + it("renders the file name and children", () => { + render( + + content + + ); + + expect(screen.getByText("report.pdf")).toBeInTheDocument(); + expect(screen.getByText("content")).toBeInTheDocument(); + }); +}); + +describe("BaseControlViewer", () => { + it("renders the file name, custom controls and children", () => { + render( + custom}> + page content + + ); + + expect(screen.getByText("report.pdf")).toBeInTheDocument(); + expect(screen.getByText("custom")).toBeInTheDocument(); + expect(screen.getByText("page content")).toBeInTheDocument(); + }); + + it("downloads the file in a new window when the download button is clicked", () => { + const openSpy = jest.spyOn(window, "open").mockImplementation(() => null); + + render( + + page content + + ); + + fireEvent.click(screen.getByLabelText("Download")); + + expect(openSpy).toHaveBeenCalledTimes(1); + const [url, windowName] = openSpy.mock.calls[0]; + expect(windowName).toBe("mendix_file"); + expect(url!.toString()).toBe("https://apps.example.com/file/42.pdf?target=window"); + openSpy.mockRestore(); + }); + + it("exposes zoom controls", () => { + render( + + page content + + ); + + expect(screen.getByLabelText("Zoom in")).toBeInTheDocument(); + expect(screen.getByLabelText("Zoom out")).toBeInTheDocument(); + expect(screen.getByLabelText("Fit to width")).toBeInTheDocument(); + }); +}); diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/PDFViewer.spec.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/PDFViewer.spec.tsx new file mode 100644 index 0000000000..c9d864b295 --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/src/components/__tests__/PDFViewer.spec.tsx @@ -0,0 +1,302 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { Document, pdfjs } from "react-pdf"; +import PDFViewer from "../PDFViewer"; +import type { DocumentRendererProps } from "../documentRenderer"; + +jest.mock("react-pdf", () => ({ + Document: jest.fn((props: { children?: unknown }) => props.children ?? null), + Page: jest.fn(() => null), + pdfjs: { + GlobalWorkerOptions: {} as { workerSrc: string }, + version: "4.8.69" + } +})); + +function buildProps(overrides: Partial = {}): DocumentRendererProps { + return { + file: { + status: "available", + value: { uri: "https://apps.example.com/file/123.pdf", name: "file.pdf" } + }, + pdfjsWorkerUrl: { status: "available", value: "https://apps.example.com/worker.js" }, + setDocumentStatus: jest.fn(), + documentStatus: { status: "available" as never }, + ...overrides + } as unknown as DocumentRendererProps; +} + +function lastDocumentProps(): { + options: { cMapUrl: string; standardFontDataUrl: string }; + onLoadSuccess: (args: { numPages: number }) => void; + onLoadError: () => void; +} { + const calls = (Document as unknown as jest.Mock).mock.calls; + return calls[calls.length - 1][0]; +} + +describe("PDFViewer resource URL resolution", () => { + const originalMx = window.mx; + + afterEach(() => { + window.mx = originalMx; + }); + + /** + * Re-evaluates the PDFViewer module against the current `window` state and + * returns the resource `options` passed down to react-pdf's `Document`. + * The `origin` constant is computed at module load, so the module must be + * re-imported between cases for the different `window.mx` states to take effect. + */ + function loadOptions(): { cMapUrl: string; standardFontDataUrl: string } { + jest.resetModules(); + // Re-require React, RTL, react-pdf and PDFViewer together so they all share + // the same freshly-evaluated React instance (resetModules resets React too). + /* eslint-disable @typescript-eslint/no-require-imports */ + const { createElement } = require("react"); + const { render: renderFresh } = require("@testing-library/react"); + const reactPdf = require("react-pdf"); + const FreshPDFViewer = require("../PDFViewer").default; + /* eslint-enable @typescript-eslint/no-require-imports */ + + renderFresh(createElement(FreshPDFViewer, buildProps())); + + const calls = (reactPdf.Document as jest.Mock).mock.calls; + return calls[calls.length - 1][0].options; + } + + it("uses mx.appUrl to build absolute cMap and standard font URLs", () => { + window.mx = { appUrl: "https://apps.example.com/my-app" }; + + expect(loadOptions()).toEqual({ + cMapUrl: "https://apps.example.com/my-app/widgets/com/mendix/shared/pdfjs/cmaps/", + standardFontDataUrl: "https://apps.example.com/my-app/widgets/com/mendix/shared/pdfjs/standard_fonts/" + }); + }); + + it("strips a trailing slash from mx.appUrl to avoid double slashes", () => { + window.mx = { appUrl: "https://apps.example.com/my-app/" }; + + expect(loadOptions()).toEqual({ + cMapUrl: "https://apps.example.com/my-app/widgets/com/mendix/shared/pdfjs/cmaps/", + standardFontDataUrl: "https://apps.example.com/my-app/widgets/com/mendix/shared/pdfjs/standard_fonts/" + }); + }); + + it("falls back to window.location.origin when mx is absent", () => { + window.mx = undefined; + + expect(loadOptions()).toEqual({ + cMapUrl: `${window.location.origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, + standardFontDataUrl: `${window.location.origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` + }); + }); + + it("falls back to window.location.origin when mx.appUrl is undefined", () => { + window.mx = {}; + + expect(loadOptions()).toEqual({ + cMapUrl: `${window.location.origin}/widgets/com/mendix/shared/pdfjs/cmaps/`, + standardFontDataUrl: `${window.location.origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/` + }); + }); +}); + +describe("PDFViewer worker resolution", () => { + it("uses the provided worker url when available", () => { + render(); + + expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe("https://apps.example.com/worker.js"); + }); + + it("falls back to the unpkg cdn worker when no url is provided", () => { + render(); + + expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe( + `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs` + ); + }); + + it("reports an error and uses no worker when the worker is unavailable", () => { + const setDocumentStatus = jest.fn(); + + render( + + ); + + expect(setDocumentStatus).toHaveBeenCalledWith({ + status: "error", + message: "Failed to load PDF document : pdfjsWorker unavailable" + }); + expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(""); + }); + + it("uses no worker when the worker status is loading", () => { + render(); + + expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(""); + }); +}); + +describe("PDFViewer rendering", () => { + it("shows a placeholder when no document is selected", () => { + render(); + + expect(screen.getByText("No document selected")).toBeInTheDocument(); + }); + + it("renders the file name and pagination controls", () => { + render(); + + expect(screen.getByText("file.pdf")).toBeInTheDocument(); + expect(screen.getByLabelText("Page number")).toBeInTheDocument(); + expect(screen.getByLabelText("Go to next page")).toBeInTheDocument(); + }); + + it("updates the total page count when the document loads", () => { + render(); + + act(() => lastDocumentProps().onLoadSuccess({ numPages: 7 })); + + expect(screen.getByText("/ 7")).toBeInTheDocument(); + }); + + it("reports an error when the document fails to load", () => { + const setDocumentStatus = jest.fn(); + render(); + + act(() => lastDocumentProps().onLoadError()); + + expect(setDocumentStatus).toHaveBeenCalledWith({ + status: "error", + message: "Failed to load PDF document" + }); + }); +}); + +describe("PDFViewer pagination", () => { + function renderWithPages(numPages: number): void { + render(); + act(() => lastDocumentProps().onLoadSuccess({ numPages })); + } + + it("disables the previous button on the first page and enables next", () => { + renderWithPages(3); + + expect(screen.getByLabelText("Go to previous page")).toBeDisabled(); + expect(screen.getByLabelText("Go to next page")).toBeEnabled(); + }); + + it("navigates to the next and previous page", () => { + renderWithPages(3); + + fireEvent.click(screen.getByLabelText("Go to next page")); + expect(screen.getByLabelText("Page number").value).toBe("2"); + + fireEvent.click(screen.getByLabelText("Go to previous page")); + expect(screen.getByLabelText("Page number").value).toBe("1"); + }); + + it("disables the next button on the last page", () => { + renderWithPages(2); + + fireEvent.click(screen.getByLabelText("Go to next page")); + + expect(screen.getByLabelText("Go to next page")).toBeDisabled(); + }); + + it("accepts only numeric input in the page field", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + fireEvent.change(input, { target: { value: "3" } }); + expect(input.value).toBe("3"); + + fireEvent.change(input, { target: { value: "abc" } }); + expect(input.value).toBe("3"); + }); + + it("jumps to a valid page on submit", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + fireEvent.change(input, { target: { value: "4" } }); + fireEvent.submit(input.closest("form")!); + + expect(input.value).toBe("4"); + }); + + it("resets to the current page when an out-of-range page is submitted", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + fireEvent.change(input, { target: { value: "99" } }); + fireEvent.blur(input); + + expect(input.value).toBe("1"); + }); + + it("validates the page on Enter key press", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + fireEvent.change(input, { target: { value: "2" } }); + fireEvent.keyDown(input, { key: "Enter" }); + + expect(input.value).toBe("2"); + }); + + it("prevents non-numeric key presses", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + const notPrevented = fireEvent.keyDown(input, { key: "a" }); + + expect(notPrevented).toBe(false); + }); + + it("allows control keys such as Backspace", () => { + renderWithPages(5); + const input = screen.getByLabelText("Page number"); + + const notPrevented = fireEvent.keyDown(input, { key: "Backspace" }); + + expect(notPrevented).toBe(true); + }); +}); + +describe("PDFViewer toolbar actions", () => { + it("downloads the file in a new window", () => { + const openSpy = jest.spyOn(window, "open").mockImplementation(() => null); + render(); + + fireEvent.click(screen.getByLabelText("Download")); + + expect(openSpy).toHaveBeenCalledTimes(1); + const [url, windowName] = openSpy.mock.calls[0]; + expect(windowName).toBe("mendix_file"); + expect(url!.toString()).toBe("https://apps.example.com/file/123.pdf?target=window"); + openSpy.mockRestore(); + }); + + it("disables zoom out at the minimum and zoom in at the maximum", () => { + render(); + + const zoomOut = screen.getByLabelText("Zoom out"); + const zoomIn = screen.getByLabelText("Zoom in"); + + // Many zoom-outs hit the lower bound and disable the button. + for (let i = 0; i < 10; i++) { + fireEvent.click(zoomOut); + } + expect(zoomOut).toBeDisabled(); + + fireEvent.click(screen.getByLabelText("Fit to width")); + + // Many zoom-ins hit the upper bound and disable the button. + for (let i = 0; i < 20; i++) { + fireEvent.click(zoomIn); + } + expect(zoomIn).toBeDisabled(); + }); +}); diff --git a/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/dimension.spec.ts b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/dimension.spec.ts new file mode 100644 index 0000000000..11f163da0f --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/dimension.spec.ts @@ -0,0 +1,71 @@ +import { constructWrapperStyle, DimensionContainerProps } from "../dimension"; + +const baseProps: DimensionContainerProps = { + widthUnit: "pixels", + width: 300, + heightUnit: "pixels", + height: 400, + minHeightUnit: "none", + minHeight: 0, + maxHeightUnit: "none", + maxHeight: 0, + overflowY: "auto" +}; + +describe("constructWrapperStyle", () => { + it("builds pixel width and height", () => { + expect(constructWrapperStyle(baseProps)).toEqual({ + width: "300px", + height: "400px" + }); + }); + + it("builds percentage width", () => { + expect(constructWrapperStyle({ ...baseProps, widthUnit: "percentage", width: 80 })).toMatchObject({ + width: "80%" + }); + }); + + it("uses fit-content for contentFit width", () => { + expect(constructWrapperStyle({ ...baseProps, widthUnit: "contentFit" })).toMatchObject({ + width: "fit-content" + }); + }); + + it("sets height to auto for percentageOfWidth without min/max constraints", () => { + expect(constructWrapperStyle({ ...baseProps, heightUnit: "percentageOfWidth" })).toEqual({ + width: "300px", + height: "auto" + }); + }); + + it("applies min height when minHeightUnit is set", () => { + const style = constructWrapperStyle({ + ...baseProps, + heightUnit: "percentageOfWidth", + minHeightUnit: "pixels", + minHeight: 120 + }); + + expect(style.minHeight).toBe("120px"); + }); + + it("applies max height and overflow when maxHeightUnit is set", () => { + const style = constructWrapperStyle({ + ...baseProps, + heightUnit: "percentageOfWidth", + maxHeightUnit: "percentageOfView", + maxHeight: 50, + overflowY: "scroll" + }); + + expect(style.maxHeight).toBe("50vh"); + expect(style.overflowY).toBe("scroll"); + }); + + it("maps percentageOfParent height unit to a percentage value", () => { + expect(constructWrapperStyle({ ...baseProps, heightUnit: "percentageOfParent", height: 75 })).toMatchObject({ + height: "75%" + }); + }); +}); diff --git a/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/helpers.spec.ts b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/helpers.spec.ts new file mode 100644 index 0000000000..bcdb0c7f73 --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/helpers.spec.ts @@ -0,0 +1,36 @@ +import { downloadFile } from "../helpers"; + +describe("downloadFile", () => { + let openSpy: jest.SpyInstance; + + beforeEach(() => { + openSpy = jest.spyOn(window, "open").mockImplementation(() => null); + }); + + afterEach(() => { + openSpy.mockRestore(); + }); + + it("does nothing when the file url is undefined", () => { + downloadFile(undefined); + + expect(openSpy).not.toHaveBeenCalled(); + }); + + it("opens the file in a dedicated window with a target query param", () => { + downloadFile("https://apps.example.com/file/123"); + + expect(openSpy).toHaveBeenCalledTimes(1); + const [url, windowName] = openSpy.mock.calls[0]; + expect(windowName).toBe("mendix_file"); + expect(url.toString()).toBe("https://apps.example.com/file/123?target=window"); + }); + + it("preserves existing query params when appending target", () => { + downloadFile("https://apps.example.com/file/123?foo=bar"); + + const [url] = openSpy.mock.calls[0]; + expect(url.searchParams.get("foo")).toBe("bar"); + expect(url.searchParams.get("target")).toBe("window"); + }); +}); diff --git a/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/useZoomScale.spec.ts b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/useZoomScale.spec.ts new file mode 100644 index 0000000000..458f7fbc0d --- /dev/null +++ b/packages/pluggableWidgets/document-viewer-web/src/utils/__tests__/useZoomScale.spec.ts @@ -0,0 +1,59 @@ +import { renderHook, act } from "@testing-library/react"; +import { useZoomScale } from "../useZoomScale"; + +describe("useZoomScale", () => { + it("starts at a zoom level of 1", () => { + const { result } = renderHook(() => useZoomScale()); + + expect(result.current.zoomLevel).toBe(1); + }); + + it("zooms in by a factor of 1.2", () => { + const { result } = renderHook(() => useZoomScale()); + + act(() => result.current.zoomIn()); + + expect(result.current.zoomLevel).toBeCloseTo(1.2); + }); + + it("zooms out by a factor of 0.8", () => { + const { result } = renderHook(() => useZoomScale()); + + act(() => result.current.zoomOut()); + + expect(result.current.zoomLevel).toBeCloseTo(0.8); + }); + + it("does not zoom in beyond the maximum of 10", () => { + const { result } = renderHook(() => useZoomScale()); + + act(() => { + for (let i = 0; i < 50; i++) { + result.current.zoomIn(); + } + }); + + expect(result.current.zoomLevel).toBe(10); + }); + + it("does not zoom out below the minimum of 0.3", () => { + const { result } = renderHook(() => useZoomScale()); + + act(() => { + for (let i = 0; i < 50; i++) { + result.current.zoomOut(); + } + }); + + expect(result.current.zoomLevel).toBe(0.3); + }); + + it("resets the zoom level back to 1", () => { + const { result } = renderHook(() => useZoomScale()); + + act(() => result.current.zoomIn()); + act(() => result.current.reset()); + + expect(result.current.zoomLevel).toBe(1); + }); +});