From b38912f3cb1afa1fbd9e97924c9367c5c22350c7 Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 16 Apr 2026 18:20:44 +0200 Subject: [PATCH 1/4] :wrench: Add short tag to DocherHub release (#8864) --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21c0eb6de2..053dd3ff0e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,13 +64,14 @@ jobs: echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io IMAGES=("frontend" "backend" "exporter" "storybook") + SHORT_TAG=${TAG%.*} for image in "${IMAGES[@]}"; do skopeo copy --all \ docker://$DOCKER_REGISTRY/$image:$TAG \ docker://docker.io/penpotapp/$image:$TAG - for alias in main latest; do + for alias in main latest "$SHORT_TAG"; do skopeo copy --all \ docker://$DOCKER_REGISTRY/$image:$TAG \ docker://docker.io/penpotapp/$image:$alias From eeeb698d91ad0791cb3bc519c8d3caffbe98ff4e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Apr 2026 18:05:51 +0000 Subject: [PATCH 2/4] :arrow_up: Bump opencode-ai dev dependency 1.4.3 -> 1.14.19 Signed-off-by: Andrey Antukh --- package.json | 2 +- pnpm-lock.yaml | 106 ++++++++++++++++++++++++------------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 26baa0d989..fbb4c5d92f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,6 @@ "@github/copilot": "^1.0.21", "@types/node": "^25.5.2", "esbuild": "^0.28.0", - "opencode-ai": "^1.4.3" + "opencode-ai": "^1.14.19" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 276b2891e2..32b4fa3382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^0.28.0 version: 0.28.0 opencode-ai: - specifier: ^1.4.3 - version: 1.4.3 + specifier: ^1.14.19 + version: 1.14.19 packages: @@ -227,67 +227,67 @@ packages: engines: {node: '>=18'} hasBin: true - opencode-ai@1.4.3: - resolution: {integrity: sha512-WwCSrLgJiS+sLIWoi9pa62vAw3l6VI3a+ShhjDDMUJBBG2FxU18xEhk8xhEedLMKyHo1p0nwD41+iKZ1y+rdAw==} + opencode-ai@1.14.19: + resolution: {integrity: sha512-67h56qYcJivd2U9VK8LJvyMBCc3ZE3HcJ/qL4YtaidSnjEumy4SxO+HHlDISsLase7TUQ0w1nULOAibdZxGzbQ==} hasBin: true - opencode-darwin-arm64@1.4.3: - resolution: {integrity: sha512-d/MT28Is5yhdFY+36AqKc5r31zx8lXTQIYblfn5R8kdhamXijZVGdD0pHl3eJc1ZolUHNwzg2B+IqV22uyU9GQ==} + opencode-darwin-arm64@1.14.19: + resolution: {integrity: sha512-gjJ97dTiBbCas3Y7K6KQMQm5/KNIBOM9+10KZHqNr2bQfn0N09O977ZkXoX6IVBBE2632Ahaa71d4pzLKN9ULw==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.4.3: - resolution: {integrity: sha512-WTqf7WBNRZcv6pClqnN4F7X/T/osgcPGikNHkHUSLszKWg9flqz7Z68kHR4i9ae8Bn3ke9MQRgzRdOt2PgLL0w==} + opencode-darwin-x64-baseline@1.14.19: + resolution: {integrity: sha512-UuwLpa511h7qQ+rTmOUmsHch/N4NBQT64dzg+iLWnR5yoR/81inENpbwxiS7hXpVdzCUGo/YnxI1u6SIBoMlTQ==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.4.3: - resolution: {integrity: sha512-8FUHeybVmaCYt4S2YmWcf32o/xa/ahCfI258bpWssrzs7Xg51JgUB/Csoble0I1mH7RpW39SKy/hHUtHGuJfJg==} + opencode-darwin-x64@1.14.19: + resolution: {integrity: sha512-uksrjOtWI7Ob5JvjZBSsrKuy3JVF9d89oYZYfWS5m8ordNyv1Nob39MXJXizv85ozsXjSb0rqjpJurnJw8K+tQ==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.4.3: - resolution: {integrity: sha512-3Ej2klaep8+fxcc44UyEuRpb/UFiNkwfzIDLIST83hFUtjzprjpTRqg6zHmOfzyfjNAaNpB4VZw6e9y3mGBpiQ==} + opencode-linux-arm64-musl@1.14.19: + resolution: {integrity: sha512-R1BJuBGWHfBxfKvIA/Hb4nhYaJgCKl1B+mAGNydu+z0CLtGtwU8r+kQWF/G2N0y8Vx6Y6DRfJiv1X0eZEfD1BQ==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.4.3: - resolution: {integrity: sha512-9jpVSOEF7TX3gPPAHVAsBT9XEO3LgYafI+IUmOzbBB9CDiVVNJw6JmEffmSpSxY4nkAh322xnMbNjVGEyXQBRA==} + opencode-linux-arm64@1.14.19: + resolution: {integrity: sha512-Bo+aZOppLF366mgGfK0CnIcAVy1EmsrBv93eot1CmPSN1oeud07GpGdn3Bjl5f6KuBx1io6JsvjQyHno+MH5AA==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.4.3: - resolution: {integrity: sha512-aned/3FQTHXXQv2PPKDprJwQaQkoadriQ6AByGhRl6/bHhSkhkiVl6cHHvYMKxYEwN4bVOydWhasfgm/xru/xw==} + opencode-linux-x64-baseline-musl@1.14.19: + resolution: {integrity: sha512-+K8MuGoHugtUec4P/nKcTwZFUipHfW7oPpwlIoPiAQou3bNFTzzP6rslbzzNwjXlQRsUw9GAtuIPDOCL6CkgDg==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.4.3: - resolution: {integrity: sha512-HpzdgYaI90qqt0WokcyBhadgFQ0EYMhq4TZ4EcaSPuZTssS2Drb6kp70Si54uOJL/MUAdc9+E0BYYIAdOJ6h1g==} + opencode-linux-x64-baseline@1.14.19: + resolution: {integrity: sha512-KuvITzg4iK0hdIjpNZepwu3bLZ/dUZDI6BwCoV4w//VEP1j3UfDyeS3vWghKcQLd2T1+yybuEMM/3RXcwm/lGQ==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.4.3: - resolution: {integrity: sha512-ibUevyDxVrwkp6FWu8UBCBsrzlKDT/uEug2NHCKaHIwo9uwVf5zsL/0ueHYqmH14SHK+M6wzWewYk6WuW9f0zQ==} + opencode-linux-x64-musl@1.14.19: + resolution: {integrity: sha512-0qe2+X76UJdrCdhdlJyfubMC4tveHAVxjSmPq7g9Zm95heBeJdcQDCLeyQk/lGgeXgsZzVPfLmyTWNtBvCZYFQ==} cpu: [x64] os: [linux] - opencode-linux-x64@1.4.3: - resolution: {integrity: sha512-RS6TsDqTUrW5sefxD1KD9Xy9mSYGXAlr2DlGrdi8vNm9e/Bt4r4u557VB7f/Uj2CxTt2Gf7OWl08ZoPlxMJ5Gg==} + opencode-linux-x64@1.14.19: + resolution: {integrity: sha512-2GljfL7BeG4xALBJVRwaAGCM/dzYF5aQf6bfLTKsQIl6QpLUguYSF+fkStBHLeehyqbDP5MtiEEuXjC0+mecjA==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.4.3: - resolution: {integrity: sha512-2ViH17WpIQbRVfQaOBMi49pu73gqTQYT/4/WxFjShmRagX40/KkG18fhvyDAZrBKfkhPtdwgFsFxMSYP9F6QCQ==} + opencode-windows-arm64@1.14.19: + resolution: {integrity: sha512-8/vRHe5tHexikfPceLmpjsQiEhuDTOSCSlEmP4s0Yq3UAkVaDAxpiWq7Bx4g8hjr1gzfXD9vbjV+WHq/BtMC/w==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.4.3: - resolution: {integrity: sha512-SWYDli9SAKQd/pS/hVfuq1KEsc+gnAJdv+YtBmxaHOw57y0euqLwbGFUYFq78GAMGt/RnTYWZIEUbRK/ZiX3UA==} + opencode-windows-x64-baseline@1.14.19: + resolution: {integrity: sha512-Z8imEJK/srE/r1fr7oNLvpLTeRJQyuL7vsbXvCt3T7j2Ew9BOZ7RuYa8EE0R6bNqQ+MLhBGPiAG7NWc++MgK8Q==} cpu: [x64] os: [win32] - opencode-windows-x64@1.4.3: - resolution: {integrity: sha512-UxmKDIw3t4XHST6JSUWHmSrCGIEK1LRTAOpO82HBC3XkIjH78gVIeauRR6RULjWAApmy9I1C3TukO2sDUi7Gvw==} + opencode-windows-x64@1.14.19: + resolution: {integrity: sha512-/TqGN91WiUzx7IPMPwmpMIzRixi5TMjaBcC9FH1TgD7DCqKnP6pokvu+ak0C9xwA4wKorE9PoZzeRt/+c3rDCQ==} cpu: [x64] os: [win32] @@ -434,55 +434,55 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 - opencode-ai@1.4.3: + opencode-ai@1.14.19: optionalDependencies: - opencode-darwin-arm64: 1.4.3 - opencode-darwin-x64: 1.4.3 - opencode-darwin-x64-baseline: 1.4.3 - opencode-linux-arm64: 1.4.3 - opencode-linux-arm64-musl: 1.4.3 - opencode-linux-x64: 1.4.3 - opencode-linux-x64-baseline: 1.4.3 - opencode-linux-x64-baseline-musl: 1.4.3 - opencode-linux-x64-musl: 1.4.3 - opencode-windows-arm64: 1.4.3 - opencode-windows-x64: 1.4.3 - opencode-windows-x64-baseline: 1.4.3 + opencode-darwin-arm64: 1.14.19 + opencode-darwin-x64: 1.14.19 + opencode-darwin-x64-baseline: 1.14.19 + opencode-linux-arm64: 1.14.19 + opencode-linux-arm64-musl: 1.14.19 + opencode-linux-x64: 1.14.19 + opencode-linux-x64-baseline: 1.14.19 + opencode-linux-x64-baseline-musl: 1.14.19 + opencode-linux-x64-musl: 1.14.19 + opencode-windows-arm64: 1.14.19 + opencode-windows-x64: 1.14.19 + opencode-windows-x64-baseline: 1.14.19 - opencode-darwin-arm64@1.4.3: + opencode-darwin-arm64@1.14.19: optional: true - opencode-darwin-x64-baseline@1.4.3: + opencode-darwin-x64-baseline@1.14.19: optional: true - opencode-darwin-x64@1.4.3: + opencode-darwin-x64@1.14.19: optional: true - opencode-linux-arm64-musl@1.4.3: + opencode-linux-arm64-musl@1.14.19: optional: true - opencode-linux-arm64@1.4.3: + opencode-linux-arm64@1.14.19: optional: true - opencode-linux-x64-baseline-musl@1.4.3: + opencode-linux-x64-baseline-musl@1.14.19: optional: true - opencode-linux-x64-baseline@1.4.3: + opencode-linux-x64-baseline@1.14.19: optional: true - opencode-linux-x64-musl@1.4.3: + opencode-linux-x64-musl@1.14.19: optional: true - opencode-linux-x64@1.4.3: + opencode-linux-x64@1.14.19: optional: true - opencode-windows-arm64@1.4.3: + opencode-windows-arm64@1.14.19: optional: true - opencode-windows-x64-baseline@1.4.3: + opencode-windows-x64-baseline@1.14.19: optional: true - opencode-windows-x64@1.4.3: + opencode-windows-x64@1.14.19: optional: true undici-types@7.18.2: {} From d5cf7dcf9d106833f1f2303305b0aaec559e0c92 Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Tue, 21 Apr 2026 15:39:35 +0200 Subject: [PATCH 3/4] :wrench: Add main-staging workflow --- .github/workflows/build-main-staging.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/build-main-staging.yml diff --git a/.github/workflows/build-main-staging.yml b/.github/workflows/build-main-staging.yml new file mode 100644 index 0000000000..33ee46947c --- /dev/null +++ b/.github/workflows/build-main-staging.yml @@ -0,0 +1,22 @@ +name: _MAIN-STAGING + +on: + workflow_dispatch: + schedule: + - cron: '26 5-20 * * 1-5' + +jobs: + build-bundle: + uses: ./.github/workflows/build-bundle.yml + secrets: inherit + with: + gh_ref: "main-staging" + build_wasm: "yes" + build_storybook: "yes" + + build-docker: + needs: build-bundle + uses: ./.github/workflows/build-docker.yml + secrets: inherit + with: + gh_ref: "main-staging" From aed2f8a8f801268bf1ed227e025c3a107cb54945 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 17:31:05 +0200 Subject: [PATCH 4/4] :bug: Fix removeChild errors from unmount race conditions (#8927) Guard imperative DOM operations (removeChild, RAF callbacks) against race conditions where React has already unmounted the target nodes. - assets/common.cljs: add dom/child? guard before removeChild in RAF - dynamic_modifiers.cljs: capture RAF IDs and cancel them on cleanup; add null guards for DOM nodes that may no longer exist - hooks.cljs: guard portal container removal with dom/child? check - errors.cljs: extract is-ignorable-exception? to a top-level defn and add NotFoundError/removeChild to ignorable exceptions, since these are caused by browser extensions modifying React-managed DOM - Add unit tests for is-ignorable-exception? predicate Signed-off-by: Andrey Antukh --- frontend/src/app/main/errors.cljs | 90 +++++++++++------- .../shapes/frame/dynamic_modifiers.cljs | 86 ++++++++++------- .../ui/workspace/sidebar/assets/common.cljs | 6 +- frontend/test/frontend_tests/errors_test.cljs | 95 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 5 files changed, 206 insertions(+), 73 deletions(-) create mode 100644 frontend/test/frontend_tests/errors_test.cljs diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 37177aec7d..0af0d8714c 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -408,43 +408,61 @@ (ex/print-throwable instance :prefix "Server Error")) (st/async-emit! (rt/assign-exception error))) +(defn- from-extension? + "True when the error stack trace originates from a browser extension." + [cause] + (let [stack (.-stack cause)] + (and (string? stack) + (or (str/includes? stack "chrome-extension://") + (str/includes? stack "moz-extension://"))))) + +(defn- from-posthog? + "True when the error stack trace originates from PostHog analytics." + [cause] + (let [stack (.-stack cause)] + (and (string? stack) + (str/includes? stack "posthog")))) + +(defn is-ignorable-exception? + "True when the error is known to be harmless (browser extensions, analytics, + React/extension DOM conflicts, etc.) and should NOT be surfaced to the user." + [cause] + (let [message (ex-message cause)] + (or (from-extension? cause) + (from-posthog? cause) + (= message "Possible side-effect in debug-evaluate") + (= message "Unexpected end of input") + (str/starts-with? message "invalid props on component") + (str/starts-with? message "Unexpected token ") + ;; Native AbortError DOMException: raised when an in-flight + ;; HTTP fetch is cancelled via AbortController (e.g. by an + ;; RxJS unsubscription / take-until chain). These are + ;; handled gracefully inside app.util.http/fetch and must NOT + ;; be surfaced as application errors. + (= (.-name ^js cause) "AbortError") + ;; Zone.js (injected by browser extensions such as Angular + ;; DevTools) wraps event listeners and assigns a custom + ;; .toString to its wrapper functions using + ;; Object.defineProperty. When the wrapper was previously + ;; defined with {writable: false}, a subsequent plain assignment + ;; in strict mode (our libs.js uses "use strict") throws this + ;; TypeError. This is a known Zone.js / browser-extension + ;; incompatibility and is NOT a Penpot bug. + (str/starts-with? message "Cannot assign to read only property 'toString'") + ;; NotFoundError DOMException: "Failed to execute + ;; 'removeChild' on 'Node'" — Thrown by React's commit + ;; phase when the DOM tree has been modified externally + ;; (typically by browser extensions like Grammarly, + ;; LastPass, translation tools, or ad blockers that + ;; inject/remove nodes). The entire stack trace is inside + ;; React internals (libs.js) with no application code, + ;; so there is nothing actionable on our side. React's + ;; error boundary already handles recovery. + (and (= (.-name ^js cause) "NotFoundError") + (str/includes? message "removeChild"))))) + (defonce uncaught-error-handler - (letfn [(from-extension? [cause] - (let [stack (.-stack cause)] - (and (string? stack) - (or (str/includes? stack "chrome-extension://") - (str/includes? stack "moz-extension://"))))) - - (from-posthog? [cause] - (let [stack (.-stack cause)] - (and (string? stack) - (str/includes? stack "posthog")))) - - (is-ignorable-exception? [cause] - (let [message (ex-message cause)] - (or (from-extension? cause) - (from-posthog? cause) - (= message "Possible side-effect in debug-evaluate") - (= message "Unexpected end of input") - (str/starts-with? message "invalid props on component") - (str/starts-with? message "Unexpected token ") - ;; Native AbortError DOMException: raised when an in-flight - ;; HTTP fetch is cancelled via AbortController (e.g. by an - ;; RxJS unsubscription / take-until chain). These are - ;; handled gracefully inside app.util.http/fetch and must NOT - ;; be surfaced as application errors. - (= (.-name ^js cause) "AbortError") - ;; Zone.js (injected by browser extensions such as Angular - ;; DevTools) wraps event listeners and assigns a custom - ;; .toString to its wrapper functions using - ;; Object.defineProperty. When the wrapper was previously - ;; defined with {writable: false}, a subsequent plain assignment - ;; in strict mode (our libs.js uses "use strict") throws this - ;; TypeError. This is a known Zone.js / browser-extension - ;; incompatibility and is NOT a Penpot bug. - (str/starts-with? message "Cannot assign to read only property 'toString'")))) - - (on-unhandled-error [event] + (letfn [(on-unhandled-error [event] (.preventDefault ^js event) (when-let [cause (unchecked-get event "error")] (when-not (is-ignorable-exception? cause) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 71533852e8..90b27f6ee2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -265,54 +265,68 @@ prev-transforms (mf/use-var nil)] (mf/with-effect [add-children] - (ts/raf - #(doseq [{:keys [shape]} add-children-prev] - (let [shape-node (get-shape-node shape) - mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))] - (when mirror-node (.remove mirror-node)) - (dom/remove-attribute! (dom/get-parent shape-node) "display")))) + (let [raf-id1 + (ts/raf + #(doseq [{:keys [shape]} add-children-prev] + (let [shape-node (get-shape-node shape) + mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))] + (when mirror-node (.remove mirror-node)) + (when-let [parent (some-> shape-node dom/get-parent)] + (dom/remove-attribute! parent "display"))))) - (ts/raf - #(doseq [{:keys [frame shape]} add-children] - (let [frame-node (get-shape-node frame) - shape-node (get-shape-node shape) + raf-id2 + (ts/raf + #(doseq [{:keys [frame shape]} add-children] + (let [frame-node (get-shape-node frame) + shape-node (get-shape-node shape)] + (when (and (some? frame-node) (some? shape-node)) + (let [clip-id + (-> (dom/query frame-node ":scope > defs > .frame-clip-def") + (dom/get-attribute "id")) - clip-id - (-> (dom/query frame-node ":scope > defs > .frame-clip-def") - (dom/get-attribute "id")) + use-node + (dom/create-element "http://www.w3.org/2000/svg" "use") - use-node - (dom/create-element "http://www.w3.org/2000/svg" "use") + contents-node + (or (dom/query frame-node ".frame-children") frame-node)] - contents-node - (or (dom/query frame-node ".frame-children") frame-node)] - - (dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape)) - (dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id)) - (dom/add-class! use-node "mirror-shape") - (dom/append-child! contents-node use-node) - (dom/set-attribute! (dom/get-parent shape-node) "display" "none"))))) + (dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape)) + (dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id)) + (dom/add-class! use-node "mirror-shape") + (dom/append-child! contents-node use-node) + (dom/set-attribute! (dom/get-parent shape-node) "display" "none"))))))] + (fn [] + (js/cancelAnimationFrame raf-id1) + (js/cancelAnimationFrame raf-id2)))) (mf/with-effect [transforms] (let [curr-shapes-set (into #{} (map :id) shapes) prev-shapes-set (into #{} (map :id) @prev-shapes) new-shapes (->> shapes (remove #(contains? prev-shapes-set (:id %)))) - removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %))))] + removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %)))) - ;; NOTE: we schedule the dom modifications to be executed - ;; asynchronously for avoid component flickering when react18 - ;; is used. + ;; NOTE: we schedule the dom modifications to be executed + ;; asynchronously for avoid component flickering when react18 + ;; is used. - (when (d/not-empty? new-shapes) - (ts/raf #(start-transform! node new-shapes))) + raf-id1 + (when (d/not-empty? new-shapes) + (ts/raf #(start-transform! node new-shapes))) - (when (d/not-empty? shapes) - (ts/raf #(update-transform! node shapes transforms modifiers))) + raf-id2 + (when (d/not-empty? shapes) + (ts/raf #(update-transform! node shapes transforms modifiers))) - (when (d/not-empty? removed-shapes) - (ts/raf #(remove-transform! node removed-shapes)))) + raf-id3 + (when (d/not-empty? removed-shapes) + (ts/raf #(remove-transform! node removed-shapes)))] - (reset! prev-modifiers modifiers) - (reset! prev-transforms transforms) - (reset! prev-shapes shapes)))) + (reset! prev-modifiers modifiers) + (reset! prev-transforms transforms) + (reset! prev-shapes shapes) + + (fn [] + (when raf-id1 (js/cancelAnimationFrame raf-id1)) + (when raf-id2 (js/cancelAnimationFrame raf-id2)) + (when raf-id3 (js/cancelAnimationFrame raf-id3))))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index 9f7762b861..cae198ad9a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -242,7 +242,11 @@ ;; afterwards, in the next render cycle. (dom/append-child! item-el counter-el) (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(.removeChild ^js item-el counter-el)))) + ;; Guard against race condition: if the user navigates away + ;; before the RAF fires, item-el may have been unmounted and + ;; counter-el is no longer a child — removeChild would throw. + (ts/raf #(when (dom/child? counter-el item-el) + (dom/remove-child! item-el counter-el))))) (defn on-asset-drag-start [event file-id asset selected item-ref asset-type on-drag-start] diff --git a/frontend/test/frontend_tests/errors_test.cljs b/frontend/test/frontend_tests/errors_test.cljs new file mode 100644 index 0000000000..8d217fca04 --- /dev/null +++ b/frontend/test/frontend_tests/errors_test.cljs @@ -0,0 +1,95 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.errors-test + (:require + [app.main.errors :as errors] + [cljs.test :as t :include-macros true])) + +(defn- make-error + "Create a JS Error-like object with the given name, message, and optional stack." + [error-name message & {:keys [stack] :or {stack ""}}] + (let [err (js/Error. message)] + (set! (.-name err) error-name) + (when (some? stack) + (set! (.-stack err) stack)) + err)) + +;; --------------------------------------------------------------------------- +;; is-ignorable-exception? tests +;; --------------------------------------------------------------------------- + +(t/deftest test-ignorable-chrome-extension + (t/testing "Errors from Chrome extensions are ignorable" + (let [cause (make-error "Error" "some error" + :stack "Error: some error\n at chrome-extension://abc123/content.js:1:1")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-moz-extension + (t/testing "Errors from Firefox extensions are ignorable" + (let [cause (make-error "Error" "some error" + :stack "Error: some error\n at moz-extension://abc123/content.js:1:1")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-posthog + (t/testing "Errors from PostHog are ignorable" + (let [cause (make-error "Error" "some error" + :stack "Error: some error\n at https://app.posthog.com/static/array.js:1:1")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-debug-evaluate + (t/testing "Debug-evaluate side-effect errors are ignorable" + (let [cause (make-error "Error" "Possible side-effect in debug-evaluate")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-unexpected-end-of-input + (t/testing "Unexpected end of input errors are ignorable" + (let [cause (make-error "SyntaxError" "Unexpected end of input")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-invalid-props + (t/testing "Invalid React props errors are ignorable" + (let [cause (make-error "Error" "invalid props on component Foo")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-unexpected-token + (t/testing "Unexpected token errors are ignorable" + (let [cause (make-error "SyntaxError" "Unexpected token <")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-abort-error + (t/testing "AbortError DOMException is ignorable" + (let [cause (make-error "AbortError" "The operation was aborted")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-zone-js-tostring + (t/testing "Zone.js toString read-only property error is ignorable" + (let [cause (make-error "TypeError" + "Cannot assign to read only property 'toString' of function 'function () { [native code] }'")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-ignorable-not-found-error-remove-child + (t/testing "NotFoundError with removeChild message is ignorable" + (let [cause (make-error "NotFoundError" + "Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node." + :stack "NotFoundError: Failed to execute 'removeChild'\n at zLe (libs.js:1:1)")] + (t/is (true? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-not-ignorable-not-found-error-other + (t/testing "NotFoundError without removeChild is NOT ignorable" + (let [cause (make-error "NotFoundError" + "Failed to execute 'insertBefore' on 'Node': something else")] + (t/is (false? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-not-ignorable-regular-error + (t/testing "Regular application errors are NOT ignorable" + (let [cause (make-error "Error" "Cannot read property 'x' of undefined")] + (t/is (false? (errors/is-ignorable-exception? cause)))))) + +(t/deftest test-not-ignorable-type-error + (t/testing "Regular TypeError is NOT ignorable" + (let [cause (make-error "TypeError" "undefined is not a function")] + (t/is (false? (errors/is-ignorable-exception? cause)))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 003e68264c..13e2796391 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -7,6 +7,7 @@ [frontend-tests.data.workspace-colors-test] [frontend-tests.data.workspace-texts-test] [frontend-tests.data.workspace-thumbnails-test] + [frontend-tests.errors-test] [frontend-tests.helpers-shapes-test] [frontend-tests.logic.comp-remove-swap-slots-test] [frontend-tests.logic.components-and-tokens] @@ -42,6 +43,7 @@ (t/run-tests 'frontend-tests.basic-shapes-test 'frontend-tests.data.repo-test + 'frontend-tests.errors-test 'frontend-tests.main-errors-test 'frontend-tests.data.viewer-test 'frontend-tests.data.workspace-colors-test