@poveste/app 0.10.0 → 0.11.0

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.
@@ -7,6 +7,7 @@ import { mapFile } from "./util/mapping.js";
7
7
  import GenericRenderStory_default from "./components/story/GenericRenderStory.js";
8
8
  import { createStateBridge } from "./util/state-bridge.js";
9
9
  import { toRawDeep } from "./util/state.js";
10
+ import { isBenignResizeObserverEvent } from "./util/uncaught.js";
10
11
  import { usePreferredDark } from "@poveste/vendors/vue-use";
11
12
  import { computed, createApp, h, onMounted, ref, watch } from "@poveste/vendors/vue";
12
13
  import { createPinia } from "@poveste/vendors/pinia";
@@ -36,7 +37,9 @@ function reportUncaught(error) {
36
37
  variantId: variantId.value
37
38
  });
38
39
  }
39
- window.addEventListener("error", (event) => reportUncaught(event.error ?? event.message));
40
+ window.addEventListener("error", (event) => {
41
+ if (!isBenignResizeObserverEvent(event)) reportUncaught(event.error ?? event.message);
42
+ });
40
43
  window.addEventListener("unhandledrejection", (event) => reportUncaught(event.reason));
41
44
  var prefersDark = usePreferredDark();
42
45
  var previewDark = computed(() => resolvePreviewDark(receivedSettings.colorScheme, prefersDark.value, isDark.value));
@@ -0,0 +1,14 @@
1
+ //#region src/app/util/uncaught.ts
2
+ var RESIZE_OBSERVER_LOOP = ["ResizeObserver loop limit exceeded", "ResizeObserver loop completed with undelivered notifications"];
3
+ /**
4
+ * A ResizeObserver loop notification is the browser saying it deferred a resize
5
+ * callback to the next frame, not a component throwing. It reaches the `error`
6
+ * event with a null `error`, so reporting `event.error ?? event.message` blames
7
+ * the story that happens to be rendering for it (#468). Requiring the null keeps
8
+ * a genuine `throw` of the same text reportable.
9
+ */
10
+ function isBenignResizeObserverEvent({ error, message }) {
11
+ return error == null && typeof message === "string" && RESIZE_OBSERVER_LOOP.some((known) => message.startsWith(known));
12
+ }
13
+ //#endregion
14
+ export { isBenignResizeObserverEvent };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/app",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "description": "Pre-bundled UI for Poveste",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -40,9 +40,9 @@
40
40
  "@fontsource/noto-sans-display": "^5.3.0",
41
41
  "fuse.js": "^7.0.0",
42
42
  "shiki": "^3.0.0",
43
- "@poveste/controls": "^0.10.0",
44
- "@poveste/shared": "^0.10.0",
45
- "@poveste/vendors": "^0.10.0"
43
+ "@poveste/controls": "^0.11.0",
44
+ "@poveste/shared": "^0.11.0",
45
+ "@poveste/vendors": "^0.11.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@iconify-json/carbon": "^1.2.25",
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isBenignResizeObserverEvent } from '../app/util/uncaught'
3
+
4
+ /**
5
+ * The shape the sandbox's `error` listener sees. A browser-emitted
6
+ * ResizeObserver notification carries a null `error`; a real throw carries the
7
+ * `Error` that was thrown.
8
+ */
9
+ const notification = (message: string) => ({ error: null, message })
10
+ const thrown = (message: string) => ({ error: new Error(message), message: `Uncaught Error: ${message}` })
11
+
12
+ describe('a ResizeObserver loop notification', () => {
13
+ // Chrome's exact wording, trailing period included, as captured from a live
14
+ // observer loop.
15
+ it('is recognised as benign with the period Chrome sends', () => {
16
+ expect(isBenignResizeObserverEvent(notification('ResizeObserver loop completed with undelivered notifications.'))).toBe(true)
17
+ })
18
+
19
+ it('is recognised as benign without the period Firefox omits', () => {
20
+ expect(isBenignResizeObserverEvent(notification('ResizeObserver loop completed with undelivered notifications'))).toBe(true)
21
+ })
22
+
23
+ it('is recognised as benign in the older loop limit wording', () => {
24
+ expect(isBenignResizeObserverEvent(notification('ResizeObserver loop limit exceeded'))).toBe(true)
25
+ })
26
+ })
27
+
28
+ describe('an error the sandbox exists to report', () => {
29
+ it('is not treated as benign when a component throws the same text', () => {
30
+ expect(isBenignResizeObserverEvent(thrown('ResizeObserver loop completed with undelivered notifications'))).toBe(false)
31
+ })
32
+
33
+ it('is not treated as benign when an unrelated script fails with no error object', () => {
34
+ expect(isBenignResizeObserverEvent(notification('Script error.'))).toBe(false)
35
+ })
36
+ })
@@ -14,6 +14,7 @@ import { mapFile } from './util/mapping'
14
14
  import { applyPreviewSettings, loadStoredPreviewSettings, receivedSettings } from './util/preview-settings.js'
15
15
  import { createStateBridge } from './util/state-bridge.js'
16
16
  import { toRawDeep } from './util/state.js'
17
+ import { isBenignResizeObserverEvent } from './util/uncaught.js'
17
18
 
18
19
  const query = parseQuery(window.location.search)
19
20
 
@@ -65,7 +66,11 @@ if (window.parent && window.parent !== window) {
65
66
  function reportUncaught(error: unknown) {
66
67
  reportStoryError(error, { storyId: storyId.value, variantId: variantId.value })
67
68
  }
68
- window.addEventListener('error', event => reportUncaught(event.error ?? event.message))
69
+ window.addEventListener('error', (event) => {
70
+ if (!isBenignResizeObserverEvent(event)) {
71
+ reportUncaught(event.error ?? event.message)
72
+ }
73
+ })
69
74
  window.addEventListener('unhandledrejection', event => reportUncaught(event.reason))
70
75
 
71
76
  // The story preview has its own color scheme, independent from the app chrome.
@@ -0,0 +1,19 @@
1
+ // Chrome ends the sentence with a period and Firefox does not, so these are
2
+ // prefixes rather than whole messages.
3
+ const RESIZE_OBSERVER_LOOP = [
4
+ 'ResizeObserver loop limit exceeded',
5
+ 'ResizeObserver loop completed with undelivered notifications',
6
+ ]
7
+
8
+ /**
9
+ * A ResizeObserver loop notification is the browser saying it deferred a resize
10
+ * callback to the next frame, not a component throwing. It reaches the `error`
11
+ * event with a null `error`, so reporting `event.error ?? event.message` blames
12
+ * the story that happens to be rendering for it (#468). Requiring the null keeps
13
+ * a genuine `throw` of the same text reportable.
14
+ */
15
+ export function isBenignResizeObserverEvent({ error, message }: { error?: unknown, message?: unknown }): boolean {
16
+ return error == null
17
+ && typeof message === 'string'
18
+ && RESIZE_OBSERVER_LOOP.some(known => message.startsWith(known))
19
+ }