JS / Web API Audit — non-Chromium Browser Compatibility¶
Temporary audit document — supports the planning report at
../non-chromium-browser-compatibility-analysis.md. Delete once findings are tracked in tickets.
Scope: TypeScript / JavaScript files under
src/services/web/src/ (Angular 21 SPA). Searches did not turn up a
projects/ directory; the workspace is a single Angular project. node_modules,
dist, and .angular were excluded.
Summary¶
- Total findings: 14
- Severity breakdown: critical 2 / major 6 / minor 6
- Browsers affected: Safari 11 / Firefox 11 / both 9
The most acute issue is study-table.effects.ts#downloadFile which calls IE-only
msSaveOrOpenBlob as a function with no guard — every non-IE browser will throw
a TypeError if this method runs (Safari and Firefox alike). Although the
method does not appear to be invoked from current call-sites, leaving it on a
hot service file is a footgun.
The second-most-acute is the HTML5 drag-and-drop in
annotation-question-tree-drag-drop.feature.ts that never calls
dataTransfer.setData() at dragstart. Firefox quietly fails to initiate the
drag in this configuration.
There are also several uses of Chromium-only navigator.userAgentData and
unawaited navigator.clipboard.writeText calls that fail silently outside
secure contexts. A pdfjs-dist v4 worker is loaded as .mjs — its support is
limited to recent Safari (16.4+) and Firefox (114+), users on older minor
versions will see PDF rendering fail.
Findings¶
F1. msSaveOrOpenBlob invoked without IE feature-check (TypeError on Safari/Firefox/Chrome)¶
- File:
src/services/web/src/app/core/services/study-table/study-table.effects.ts:74-90 - Browsers: Safari, Firefox, Chrome (any non-IE)
- Severity: critical
- Symptom: As soon as
downloadFile()runs, the lineif ((window.navigator as any).msSaveOrOpenBlob(blob, filename))throwsTypeError: window.navigator.msSaveOrOpenBlob is not a functionbecause the method only exists in IE/legacy Edge. Theelsebranch (the modern<a>.click()flow) is never reached. - Cause: Missing
typeof === 'function'guard. Compare to the correct pattern insrc/services/web/src/app/shared/utils/download-file.ts:6-9which guards viatypeof nav.msSaveOrOpenBlob === 'function'first. - Fix: Replace the body with a call to
saveBlob()from@app/shared/utils/download-file, or add the sametypeofguard. The method appears unused in the live codebase (no call sites found), so deleting it outright is also a valid option. - Effort: trivial
F2. Drag-and-drop dragstart never calls dataTransfer.setData() (Firefox)¶
- File:
src/services/web/src/app/project/project-admin/question-management/design/annotation-question-tree-drag-drop.feature.ts:509-527 - Browsers: Firefox
- Severity: critical
- Symptom: Question-tree drag-and-drop reordering does not initiate, or drops are silently ignored, in Firefox.
- Cause: Firefox's HTML5 drag-and-drop spec compliance is strict: a
dragstarthandler must callevent.dataTransfer.setData(format, value)at least once or the drag is not registered. The handler only setseffectAllowedand a drag image. - Fix: Add
dragEvent.dataTransfer!.setData('text/plain', draggedQuestionId)early indragStart(beforesetDragImage). The value can be the question ID since that's what the drop handler resolves anyway. - Effort: trivial
F3. navigator.userAgentData is Chromium-only — no real fallback validation¶
- File:
src/services/web/src/app/core/services/client-environment/client-environment.service.ts:31-46 - Browsers: Safari, Firefox
- Severity: major
- Symptom: "Detected browser/OS" fields shown in the Contact-Us form
(
contact-us.component.ts:341,650-654) and in the version-info dialog will fall back to UA-string parsing in Safari/Firefox. The legacy parser handles Safari but reportsChromefor Chromium-Edge first (because the regex order inparseLegacyUAisChrome→Edg); Edge UAs always containChrome/…so Edge is mis-detected (low impact). The Promise chain itself does not break. - Cause:
nav.userAgentData?.getHighEntropyValuesonly exists in Chromium. The fallback works but mislabels Edge. - Fix: Reorder the
pickcalls inparseLegacyUAsoEdg/,OPR/, thenFirefox/, thenSafari, thenChromeare tested in priority order. Optionally adduserAgentDatatype guard ('userAgentData' in navigator) to silence type warnings. - Effort: low
F4. PDF.js v4 worker as .mjs requires Safari 16.4+ / Firefox 114+¶
- File:
src/services/web/src/app/pdf-tools/pdf-loader.service.ts:5-13 - Browsers: Safari < 16.4, Firefox < 114 (module workers)
- Severity: major
- Symptom: PDF rendering in
pdf-display,pdf-page, andgraph-selectorcomponents silently produces a blank canvas / never resolves on iOS Safari 15.x, macOS Safari 15.x, and Firefox ESR <114. The underlying error isSyntaxError: import.meta.urlorDOMException: failed to construct module Worker. - Cause:
pdfjs-dist@^4.10.38ships its worker as an ES module (pdf.worker.min.mjs). Module workers are gated behind Safari 16.4 (March 2023) and Firefox 114 (June 2023). pdfjs-dist v4 also relies onPromise.withResolversandArray.prototype.at, both shipped only in Safari 16+/Firefox 113+. - Fix: Either (a) downgrade to
pdfjs-dist@3.xwhich ships a classic worker, (b) use the legacy build bundled with v4 (pdfjs-dist/legacy/build/pdf.worker.min.js), or © document the Safari 16.4+ / Firefox 114+ floor explicitly and bail out with a friendly message for older browsers. Option (b) is the smallest diff:pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/legacy/build/pdf.worker.min.js', import.meta.url).toString(). - Effort: medium
F5. crypto.subtle.digest requires a secure context¶
- File:
src/services/web/src/app/core/services/s3-file.service.ts:237-245 - Browsers: Safari, Firefox (and Chrome) when on plain HTTP
- Severity: major
- Symptom: Search-import file uploads fail silently —
generateSha256FileHash()rejects withTypeError: undefined is not an object (evaluating 'crypto.subtle.digest')on Safari, orTypeError: crypto.subtle is undefinedon Firefox, when the page is served overhttp://(onlylocalhostis whitelisted as secure insecurely). All three SyRF environments are HTTPS, but PR-preview URLs and any future internal staging served over plain HTTP would trip this. - Cause:
crypto.subtleis restricted to secure contexts in all browsers, but Safari and Firefox enforce this slightly more strictly (Chrome occasionally permits localhost / 127.0.0.1 / file:// in dev). No fallback or guard is present. - Fix: Wrap with
if (!window.isSecureContext) throw new Error('File hash requires HTTPS — please access via the secure URL'), surface the error in the upload UI, and document the HTTPS requirement. - Effort: low
F6. Multiple unawaited / unhandled navigator.clipboard.writeText calls¶
- Files:
src/services/web/src/app/manage/profile/profile.component.ts:264src/services/web/src/app/project/project-overview/project-overview.component.ts:175src/services/web/src/app/studies/study-table/study-table.component.ts:732src/services/web/src/app/project/project-admin/systematic-searches/systematic-searches.component.ts:156- Browsers: Safari, Firefox (failure modes differ)
- Severity: major
- Symptom: "Copied to clipboard" snackbars appear even when the underlying copy failed. Failure modes:
- Safari (esp. iOS) requires the call to happen synchronously inside a
user-gesture handler. RxJS / NgRx async pipelines that defer the call by a
microtask trip
NotAllowedError. None of these call sites awaits or catches the returned Promise, so the snackbar lies. - Firefox blocks
clipboard.writeTextoutside of secure contexts and surfaces a rejection that is silently swallowed. - Cause: Each call site fires-and-forgets the Promise without
.catch()orawaitplus a fallback (e.g. the<textarea> + execCommand('copy')fallback already implemented inversion-check-dialog.component.ts:56-74andexport-error-details-dialog.component.ts:261-279). - Fix: Refactor copy-to-clipboard into a shared service (
ClipboardService) that: - Tries
navigator.clipboard.writeTextifnavigator.clipboardexists. - Falls back to the
document.execCommand('copy')textarea trick. - Returns
Promise<boolean>so callers know whether to show success or failure snackbar. - Effort: medium
F7. Date.parse(...) on user-supplied/audit timestamp strings¶
- Files:
src/services/web/src/app/core/state/entities/invitation/invitation.selectors.ts:74src/services/web/src/app/core/state/entities/membership/membership.selectors.ts:68src/services/web/src/app/core/state/entities/stage-usage/stage-usage.selectors.ts:22-23,52-53src/services/web/src/app/core/state/entities/project-usage/project-usage.selectors.ts:18-19src/services/web/src/app/core/state/entities/join-request/join-request.selectors.ts:62src/services/web/src/app/core/state/entities/project/project-detail.viewmodel.ts:71- Browsers: Safari (and historically Firefox)
- Severity: minor (assuming server returns ISO-8601)
- Symptom: Invitation, membership, stage-usage, etc. lists sort
inconsistently (some entries appear "at the start of time" because
Date.parsereturnedNaN). - Cause: The server emits ISO-8601 with
Tseparator, but if any timestamp is ever serialised in a different format (e.g."2026-05-01 12:34:56"), Safari rejects it (returnsNaN) while Chrome accepts. The selectors do not guard againstNaN. - Fix: Use
new Date(timestamp).valueOf()andisNaN-check the result, or use the project'sdate-fns(already imported indynamic-locale.ts) viaparseISO. Add explicit defaults for invalid dates. - Effort: low
F8. (window.location.reload as any)(true) — non-standard signature¶
- File:
src/services/web/src/app/core/services/signal-r/signal-r.service.ts:289 - Browsers: Firefox (since 28), Safari ignores or warns
- Severity: minor
- Symptom: When SignalR receives a
UiVersionCheckfor an upgrade, the page reload fires the deprecated booleanforceReloadargument. Firefox emits a console warning ("forceReload is no longer supported"). The reload itself still happens because the boolean is silently ignored. - Cause:
Location.reload()no longer accepts an argument since the spec was tightened ~2014. The cast toanywas added to suppress the TS error. - Fix: Replace with
window.location.reload(). To force-bypass-cache, prefer adding a cache-busting query param to a navigation, or rely on service-worker / HTTP caching headers. - Effort: trivial
F9. Top-level-await IIFE in main.ts requires ES2022 target everywhere¶
- File:
src/services/web/src/main.ts:153-490 - Browsers: Safari < 15, Firefox < 89
- Severity: minor (Angular CLI normally transpiles)
- Symptom: If the build pipeline ever fell back to a lower target, Safari
14.x and Firefox <89 would refuse to parse the bundle (
SyntaxError: unexpected token 'await'). - Cause:
tsconfig.json:41sets"target": "ES2022". Angular's bundler usually downlevels to ES2017, but if a custom webpack rule (the workspace hascustom-webpack.config.js) leaks the ES2022 target into the production bundle, async-IIFE inside a non-async wrapper is the failure point. - Fix: Verify
angular.json's production build target /browserslistoutput. Add> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11if no.browserslistrcfile is present (none was found). The standard async IIFE form(async () => {...})()is fine for ES2017+. - Effort: low (verify only)
F10. document.execCommand('copy') deprecation¶
- Files:
src/services/web/src/app/project/data-export/shared/export-error-details-dialog/export-error-details-dialog.component.ts:272src/services/web/src/app/core/components/version-check-dialog/version-check-dialog.component.ts:67- Browsers: Safari, Firefox (warning only today)
- Severity: minor
- Symptom: Console warnings;
execCommand('copy')still works as a fallback today but is deprecated. Firefox 127+ emits a DevTools deprecation warning. Safari 17.4+ has begun returningfalseon cross-origin iframes. - Cause: Used as fallback when
navigator.clipboardis unavailable. - Fix: Keep as last-resort fallback for now, but track the deprecation.
Once F6 is consolidated into a
ClipboardService, the fallback lives in exactly one place where it is easy to swap when browsers drop support. - Effort: trivial
F11. ResizeObserver — no polyfill or feature-check¶
- Files:
src/services/web/src/app/project/project.component.ts:42,56src/services/web/src/app/core/services/layout/layout.service.ts:32-84- Browsers: Safari < 13.1, Firefox < 69
- Severity: minor (modern Safari/Firefox fine)
- Symptom: On older iOS Safari (< 13.1) the constructor throws
ReferenceError: ResizeObserver is not defined, breaking the project shell layout. - Cause: No
typeof ResizeObserver === 'undefined'guard. - Fix: Wrap construction in
if (typeof ResizeObserver !== 'undefined') { … }. If the project already drops support for Safari < 14 inpackage.json/CI, document that inpolyfills.ts:11(the comment still says "Safari >= 10"). - Effort: trivial
F12. polyfills.ts comment is stale and contains no actual polyfills¶
- File:
src/services/web/src/polyfills.ts:1-58 - Browsers: all
- Severity: minor
- Symptom: Documentation drift — the file claims to support "Safari >= 10, Chrome >= 55, Edge >= 13" but no compatibility shims are imported. Maintainers may believe older browsers are still tested.
- Cause: Boilerplate left over from Angular CLI scaffolding.
- Fix: Either add the polyfills for the documented baseline (or, more
practically, shrink the support matrix in the comment to match modern
Angular 21 baseline: Chrome ≥120, Edge ≥120, Safari ≥17, Firefox ≥130 — see
Angular 21 release notes). Pair with adding a
.browserslistrcfile in the web service root. - Effort: low
F13. tsconfig.json declares "types": ["user-agent-data-types"]¶
- File:
src/services/web/tsconfig.json:26-29 - Browsers: Safari, Firefox
- Severity: minor
- Symptom: Encourages further use of Chromium-only
navigator.userAgentDatabecause the type is globally available. Already noted at F3. - Cause: The type package was added when
client-environment.service.tswas first written. Removing it forces a code review when developers reach for the API. - Fix: Either remove the global types entry and import the types locally
in
client-environment.service.ts, or leave it but add an ESLint rule banningnavigator.userAgentDataoutside that one file. - Effort: trivial
F14. PDF rendering uses import.meta.url which is ES Modules-only¶
- File:
src/services/web/src/app/pdf-tools/pdf-loader.service.ts:8-12 - Browsers: Safari < 12.1, Firefox < 60
- Severity: minor (related to F4)
- Symptom: Any browser that does not support
import.metacannot resolve the worker URL. In practice this overlaps with F4 — anything modern enough to run pdfjs-dist@4 supportsimport.meta.url. - Cause:
import.meta.urlis an ES Modules feature. Angular CLI's application builder emits ES modules in the production bundle, so end-users on Safari 12 / Firefox 59 cannot run the app at all. - Fix: Once F4 is fixed (legacy worker), remove
import.meta.urland point at the static asset path served by Angular (e.g.'/assets/pdf.worker.min.js'). - Effort: low (fix together with F4)
Patterns observed¶
- Inconsistent clipboard handling: 8 distinct call sites use
navigator.clipboard.writeText, only 2 of them have a fallback path, and none of them shares code. Consolidating into a singleClipboardServiceresolves F6 + F10 in one go. - Stale IE compatibility shims:
msSaveOrOpenBlobreferences in two places, only one with proper feature-detection. Modern browsers do not need this fallback and the broken site (F1) actively crashes outside IE. - Chromium-first feature use without explicit polyfills:
navigator.userAgentData(F3), pdfjs-dist v4 module worker (F4 / F14),crypto.subtle(F5) — together these establish a soft floor of "modern Chrome" without surfacing requirements to users on Safari 15.x or Firefox ESR. - Missing
.browserslistrc: The repository root has no browserslist configuration. Adding one would make the support matrix explicit and let Angular's bundler decide on transpilation. - No SignalR transport forcing: Good news —
signal-r.service.ts:196-199uses default transport selection, so SignalR negotiates WebSockets first then falls back to ServerSentEvents and LongPolling automatically. Safari and Firefox both speak WebSockets, so no transport-related bug exists. (No finding required, noted for completeness.)
What was NOT found (negative findings)¶
For the record, the following Chrome-only APIs were searched for and did not appear in the codebase:
Element.prototype.scrollIntoViewIfNeeded— only standardscrollIntoViewused (project-options.component.ts:232).HTMLInputElement.showPicker(),showOpenFilePicker,showSaveFilePicker,showDirectoryPicker— none.navigator.share,EyeDropper,URLPattern,navigator.locks— none.webkitAudioContext,webkitFullscreen*vendor prefixes — none.BroadcastChannel,OffscreenCanvas,requestIdleCallback— none.IntersectionObserver— none in app code (only in test setup).- Service worker / Web Workers / SharedWorker (other than pdfjs) — none.
- Indexed DB direct usage — none.
- Regex
/.../dindices flag, lookbehind assertions, named-capture groups — none. Array.prototype.at,findLast,toSorted,toReversed,Object.hasOwn,structuredClone— none.MediaRecorder,AudioContext, fullscreen APIs — none.- HTML
<dialog>element — only Material'sMatDialogis used.