From deb0a936362746add87e6e24da677b3ebbdb3cce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:33:41 +0000 Subject: [PATCH 1/8] Initial plan From ee7970e8a0898992cba76972040d98e8537cb814 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:50:12 +0000 Subject: [PATCH 2/8] Fix ObjectInspector to display Symbol-keyed properties Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-inspector.test.tsx | 71 +++++++++++++++++++ .../react/js-inspector/object-inspector.tsx | 27 +++++++ .../js-inspector/utils/property-utils.tsx | 2 +- pnpm-workspace.yaml | 37 ++++++---- 4 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx new file mode 100644 index 00000000000..0d1499fb0d4 --- /dev/null +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -0,0 +1,71 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ObjectInspector } from "./object-inspector.js"; + +describe("ObjectInspector", () => { + describe("Symbol properties", () => { + it("should display Symbol-keyed properties", () => { + const sym = Symbol("testSymbol"); + const data = { + stringProp: "value1", + [sym]: "symbolValue", + }; + + const { container } = render(); + + // Check that the Symbol property is displayed + expect(container.textContent).toContain("Symbol(testSymbol)"); + expect(container.textContent).toContain("symbolValue"); + }); + + it("should display Symbol-keyed properties without description", () => { + const sym = Symbol(); + const data = { + [sym]: "symbolValue", + }; + + const { container } = render(); + + // Check that the Symbol property is displayed (even without description) + expect(container.textContent).toContain("Symbol()"); + expect(container.textContent).toContain("symbolValue"); + }); + + it("should display both string and Symbol properties", () => { + const sym1 = Symbol("first"); + const sym2 = Symbol("second"); + const data = { + stringProp1: "value1", + stringProp2: "value2", + [sym1]: "symbolValue1", + [sym2]: "symbolValue2", + }; + + const { container } = render(); + + // Check that all properties are displayed + expect(container.textContent).toContain("stringProp1"); + expect(container.textContent).toContain("value1"); + expect(container.textContent).toContain("stringProp2"); + expect(container.textContent).toContain("value2"); + expect(container.textContent).toContain("Symbol(first)"); + expect(container.textContent).toContain("symbolValue1"); + expect(container.textContent).toContain("Symbol(second)"); + expect(container.textContent).toContain("symbolValue2"); + }); + + it("should display Symbol properties with complex values", () => { + const sym = Symbol("complexSymbol"); + const data = { + [sym]: { nested: "value", count: 42 }, + }; + + const { container } = render(); + + // Check that the Symbol property is displayed + expect(container.textContent).toContain("Symbol(complexSymbol)"); + // The complex object should be represented (exact format may vary) + expect(container.textContent).toContain("Object"); + }); + }); +}); diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx index b5bf0f4af58..17c3f7649d7 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx @@ -69,6 +69,33 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = } } + // Also iterate over Symbol keys + const symbolKeys = Object.getOwnPropertySymbols(data); + for (const symbolKey of symbolKeys) { + if (propertyIsEnumerable.call(data, symbolKey)) { + const propertyValue = getPropertyValue(data, symbolKey); + yield { + name: symbolKey.toString(), + data: propertyValue, + }; + } else if (showNonenumerable) { + let propertyValue; + try { + propertyValue = getPropertyValue(data, symbolKey); + } catch (e) { + // console.warn(e) + } + + if (propertyValue !== undefined) { + yield { + name: symbolKey.toString(), + data: propertyValue, + isNonenumerable: true, + }; + } + } + } + // [[Prototype]] of the object: `Object.getPrototypeOf(data)` // the property name is shown as "__proto__" if (showNonenumerable && data !== Object.prototype /* already added */) { diff --git a/packages/html-program-viewer/src/react/js-inspector/utils/property-utils.tsx b/packages/html-program-viewer/src/react/js-inspector/utils/property-utils.tsx index 614e3975b2c..b87dc15df2a 100644 --- a/packages/html-program-viewer/src/react/js-inspector/utils/property-utils.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/utils/property-utils.tsx @@ -1,4 +1,4 @@ -export function getPropertyValue(object: any, propertyName: string) { +export function getPropertyValue(object: any, propertyName: string | symbol) { const propertyDescriptor = Object.getOwnPropertyDescriptor(object, propertyName); if (propertyDescriptor?.get) { try { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e161307424c..43c2a9278ab 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,16 +1,29 @@ packages: - - "packages/*" - - "e2e" - - "website" - - "!packages/http-client-csharp/**" - - "!packages/http-client-java/**" - - "!packages/http-client-python/**" + - packages/* + - e2e + - website + - '!packages/http-client-csharp/**' + - '!packages/http-client-java/**' + - '!packages/http-client-python/**' -overrides: - "cross-spawn@>=7.0.0 <7.0.5": "^7.0.5" - rollup: 4.49.0 # Regression in 4.50.0 https://github.com/rollup/rollup/issues/6099 +minimumReleaseAge: 2880 -# Minimum age (in minutes) for a new dependency version to be able to be used. -minimumReleaseAge: 2880 # 2 days minimumReleaseAgeExclude: - - "@alloy-js/*" + - '@alloy-js/*' + +onlyBuiltDependencies: + - '@playwright/browser-chromium' + - '@scarf/scarf' + - '@vscode/vsce-sign' + - esbuild + - keytar + - sharp + - tree-sitter-c-sharp + - tree-sitter-java + - tree-sitter-javascript + - tree-sitter-python + - tree-sitter-typescript + +overrides: + cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 + rollup: 4.49.0 From 9386116537cb0c52aef76713f01754bcee25a831 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:55:47 +0000 Subject: [PATCH 3/8] Simplify ObjectInspector tests for Symbol properties Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-inspector.test.tsx | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx index 0d1499fb0d4..bd25fe398c7 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -11,7 +11,7 @@ describe("ObjectInspector", () => { [sym]: "symbolValue", }; - const { container } = render(); + const { container } = render(); // Check that the Symbol property is displayed expect(container.textContent).toContain("Symbol(testSymbol)"); @@ -24,48 +24,11 @@ describe("ObjectInspector", () => { [sym]: "symbolValue", }; - const { container } = render(); + const { container } = render(); // Check that the Symbol property is displayed (even without description) expect(container.textContent).toContain("Symbol()"); expect(container.textContent).toContain("symbolValue"); }); - - it("should display both string and Symbol properties", () => { - const sym1 = Symbol("first"); - const sym2 = Symbol("second"); - const data = { - stringProp1: "value1", - stringProp2: "value2", - [sym1]: "symbolValue1", - [sym2]: "symbolValue2", - }; - - const { container } = render(); - - // Check that all properties are displayed - expect(container.textContent).toContain("stringProp1"); - expect(container.textContent).toContain("value1"); - expect(container.textContent).toContain("stringProp2"); - expect(container.textContent).toContain("value2"); - expect(container.textContent).toContain("Symbol(first)"); - expect(container.textContent).toContain("symbolValue1"); - expect(container.textContent).toContain("Symbol(second)"); - expect(container.textContent).toContain("symbolValue2"); - }); - - it("should display Symbol properties with complex values", () => { - const sym = Symbol("complexSymbol"); - const data = { - [sym]: { nested: "value", count: 42 }, - }; - - const { container } = render(); - - // Check that the Symbol property is displayed - expect(container.textContent).toContain("Symbol(complexSymbol)"); - // The complex object should be represented (exact format may vary) - expect(container.textContent).toContain("Object"); - }); }); }); From 1329be4deb01afeb2c6ae6ca142121ce5d60a7a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:58:28 +0000 Subject: [PATCH 4/8] Refactor to reduce code duplication and improve test coverage Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-inspector.test.tsx | 55 +++++++++++ .../react/js-inspector/object-inspector.tsx | 92 ++++++++----------- pnpm-workspace.yaml | 37 +++----- 3 files changed, 107 insertions(+), 77 deletions(-) diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx index bd25fe398c7..482258c2f0c 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -30,5 +30,60 @@ describe("ObjectInspector", () => { expect(container.textContent).toContain("Symbol()"); expect(container.textContent).toContain("symbolValue"); }); + + it("should display both string and Symbol properties", () => { + const sym1 = Symbol("first"); + const sym2 = Symbol("second"); + const data = { + stringProp1: "value1", + stringProp2: "value2", + [sym1]: "symbolValue1", + [sym2]: "symbolValue2", + }; + + const { container } = render(); + + // Check that all properties are displayed + expect(container.textContent).toContain("stringProp1"); + expect(container.textContent).toContain("value1"); + expect(container.textContent).toContain("stringProp2"); + expect(container.textContent).toContain("value2"); + expect(container.textContent).toContain("Symbol(first)"); + expect(container.textContent).toContain("symbolValue1"); + expect(container.textContent).toContain("Symbol(second)"); + expect(container.textContent).toContain("symbolValue2"); + }); + + it("should display non-enumerable Symbol properties when showNonenumerable is true", () => { + const sym = Symbol("nonEnumSymbol"); + const data = {}; + // Define a non-enumerable Symbol property + Object.defineProperty(data, sym, { + value: "nonEnumValue", + enumerable: false, + }); + + const { container } = render(); + + // Check that the non-enumerable Symbol property is displayed + expect(container.textContent).toContain("Symbol(nonEnumSymbol)"); + expect(container.textContent).toContain("nonEnumValue"); + }); + + it("should not display non-enumerable Symbol properties when showNonenumerable is false", () => { + const sym = Symbol("nonEnumSymbol"); + const data = {}; + // Define a non-enumerable Symbol property + Object.defineProperty(data, sym, { + value: "nonEnumValue", + enumerable: false, + }); + + const { container } = render(); + + // Check that the non-enumerable Symbol property is NOT displayed + expect(container.textContent).not.toContain("Symbol(nonEnumSymbol)"); + expect(container.textContent).not.toContain("nonEnumValue"); + }); }); }); diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx index 17c3f7649d7..5ef46f50613 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx @@ -8,6 +8,42 @@ import { propertyIsEnumerable } from "./utils/object-prototype.js"; import { getPropertyValue } from "./utils/property-utils.js"; const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) => { + // Helper function to handle property iteration for both string and Symbol keys + function* iterateProperties( + data: any, + keys: (string | symbol)[], + formatKey: (key: string | symbol) => string = (k) => + typeof k === "string" ? k || `""` : k.toString(), + ) { + for (const key of keys) { + if (propertyIsEnumerable.call(data, key)) { + const propertyValue = getPropertyValue(data, key); + yield { + name: formatKey(key), + data: propertyValue, + }; + } else if (showNonenumerable) { + // To work around the error (happens some time when propertyName === 'caller' || propertyName === 'arguments') + // 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context + // http://stackoverflow.com/questions/31921189/caller-and-arguments-are-restricted-function-properties-and-cannot-be-access + let propertyValue; + try { + propertyValue = getPropertyValue(data, key); + } catch (e) { + // console.warn(e) + } + + if (propertyValue !== undefined) { + yield { + name: formatKey(key), + data: propertyValue, + isNonenumerable: true, + }; + } + } + } + } + const objectIterator = function* (data: any) { const shouldIterate = (typeof data === "object" && data !== null) || typeof data === "function"; if (!shouldIterate) return; @@ -33,6 +69,7 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = i++; } } else { + // String property names const keys = Object.getOwnPropertyNames(data); if (sortObjectKeys === true && !dataIsArray) { // Array keys should not be sorted in alphabetical order @@ -41,60 +78,11 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = keys.sort(sortObjectKeys); } - for (const propertyName of keys) { - if (propertyIsEnumerable.call(data, propertyName)) { - const propertyValue = getPropertyValue(data, propertyName); - yield { - name: propertyName || `""`, - data: propertyValue, - }; - } else if (showNonenumerable) { - // To work around the error (happens some time when propertyName === 'caller' || propertyName === 'arguments') - // 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context - // http://stackoverflow.com/questions/31921189/caller-and-arguments-are-restricted-function-properties-and-cannot-be-access - let propertyValue; - try { - propertyValue = getPropertyValue(data, propertyName); - } catch (e) { - // console.warn(e) - } - - if (propertyValue !== undefined) { - yield { - name: propertyName, - data: propertyValue, - isNonenumerable: true, - }; - } - } - } + yield* iterateProperties(data, keys); - // Also iterate over Symbol keys + // Symbol property names const symbolKeys = Object.getOwnPropertySymbols(data); - for (const symbolKey of symbolKeys) { - if (propertyIsEnumerable.call(data, symbolKey)) { - const propertyValue = getPropertyValue(data, symbolKey); - yield { - name: symbolKey.toString(), - data: propertyValue, - }; - } else if (showNonenumerable) { - let propertyValue; - try { - propertyValue = getPropertyValue(data, symbolKey); - } catch (e) { - // console.warn(e) - } - - if (propertyValue !== undefined) { - yield { - name: symbolKey.toString(), - data: propertyValue, - isNonenumerable: true, - }; - } - } - } + yield* iterateProperties(data, symbolKeys); // [[Prototype]] of the object: `Object.getPrototypeOf(data)` // the property name is shown as "__proto__" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 43c2a9278ab..e161307424c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,29 +1,16 @@ packages: - - packages/* - - e2e - - website - - '!packages/http-client-csharp/**' - - '!packages/http-client-java/**' - - '!packages/http-client-python/**' + - "packages/*" + - "e2e" + - "website" + - "!packages/http-client-csharp/**" + - "!packages/http-client-java/**" + - "!packages/http-client-python/**" -minimumReleaseAge: 2880 +overrides: + "cross-spawn@>=7.0.0 <7.0.5": "^7.0.5" + rollup: 4.49.0 # Regression in 4.50.0 https://github.com/rollup/rollup/issues/6099 +# Minimum age (in minutes) for a new dependency version to be able to be used. +minimumReleaseAge: 2880 # 2 days minimumReleaseAgeExclude: - - '@alloy-js/*' - -onlyBuiltDependencies: - - '@playwright/browser-chromium' - - '@scarf/scarf' - - '@vscode/vsce-sign' - - esbuild - - keytar - - sharp - - tree-sitter-c-sharp - - tree-sitter-java - - tree-sitter-javascript - - tree-sitter-python - - tree-sitter-typescript - -overrides: - cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 - rollup: 4.49.0 + - "@alloy-js/*" From d72b5d94d76b1a1f8e00a19cfb96c8331463af7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:02:01 +0000 Subject: [PATCH 5/8] Add Symbol key sorting support for consistency Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../src/react/js-inspector/object-inspector.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx index 5ef46f50613..02e90eef52a 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx @@ -82,6 +82,10 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = // Symbol property names const symbolKeys = Object.getOwnPropertySymbols(data); + if (sortObjectKeys && !dataIsArray) { + // Sort symbols by their description for consistent display + symbolKeys.sort((a, b) => a.toString().localeCompare(b.toString())); + } yield* iterateProperties(data, symbolKeys); // [[Prototype]] of the object: `Object.getPrototypeOf(data)` From 3f442312bd7783b43c58793641bed2941cdab71e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:14:03 +0000 Subject: [PATCH 6/8] Address PR feedback: simplify code and remove nested describe Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-inspector.test.tsx | 138 +++++++++--------- .../react/js-inspector/object-inspector.tsx | 87 +++++------ 2 files changed, 104 insertions(+), 121 deletions(-) diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx index 482258c2f0c..ad9bfbae7f0 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -1,89 +1,85 @@ import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { expect, it } from "vitest"; import { ObjectInspector } from "./object-inspector.js"; -describe("ObjectInspector", () => { - describe("Symbol properties", () => { - it("should display Symbol-keyed properties", () => { - const sym = Symbol("testSymbol"); - const data = { - stringProp: "value1", - [sym]: "symbolValue", - }; +it("should display Symbol-keyed properties", () => { + const sym = Symbol("testSymbol"); + const data = { + stringProp: "value1", + [sym]: "symbolValue", + }; - const { container } = render(); + const { container } = render(); - // Check that the Symbol property is displayed - expect(container.textContent).toContain("Symbol(testSymbol)"); - expect(container.textContent).toContain("symbolValue"); - }); + // Check that the Symbol property is displayed + expect(container.textContent).toContain("Symbol(testSymbol)"); + expect(container.textContent).toContain("symbolValue"); +}); - it("should display Symbol-keyed properties without description", () => { - const sym = Symbol(); - const data = { - [sym]: "symbolValue", - }; +it("should display Symbol-keyed properties without description", () => { + const sym = Symbol(); + const data = { + [sym]: "symbolValue", + }; - const { container } = render(); + const { container } = render(); - // Check that the Symbol property is displayed (even without description) - expect(container.textContent).toContain("Symbol()"); - expect(container.textContent).toContain("symbolValue"); - }); + // Check that the Symbol property is displayed (even without description) + expect(container.textContent).toContain("Symbol()"); + expect(container.textContent).toContain("symbolValue"); +}); - it("should display both string and Symbol properties", () => { - const sym1 = Symbol("first"); - const sym2 = Symbol("second"); - const data = { - stringProp1: "value1", - stringProp2: "value2", - [sym1]: "symbolValue1", - [sym2]: "symbolValue2", - }; +it("should display both string and Symbol properties", () => { + const sym1 = Symbol("first"); + const sym2 = Symbol("second"); + const data = { + stringProp1: "value1", + stringProp2: "value2", + [sym1]: "symbolValue1", + [sym2]: "symbolValue2", + }; - const { container } = render(); + const { container } = render(); - // Check that all properties are displayed - expect(container.textContent).toContain("stringProp1"); - expect(container.textContent).toContain("value1"); - expect(container.textContent).toContain("stringProp2"); - expect(container.textContent).toContain("value2"); - expect(container.textContent).toContain("Symbol(first)"); - expect(container.textContent).toContain("symbolValue1"); - expect(container.textContent).toContain("Symbol(second)"); - expect(container.textContent).toContain("symbolValue2"); - }); + // Check that all properties are displayed + expect(container.textContent).toContain("stringProp1"); + expect(container.textContent).toContain("value1"); + expect(container.textContent).toContain("stringProp2"); + expect(container.textContent).toContain("value2"); + expect(container.textContent).toContain("Symbol(first)"); + expect(container.textContent).toContain("symbolValue1"); + expect(container.textContent).toContain("Symbol(second)"); + expect(container.textContent).toContain("symbolValue2"); +}); - it("should display non-enumerable Symbol properties when showNonenumerable is true", () => { - const sym = Symbol("nonEnumSymbol"); - const data = {}; - // Define a non-enumerable Symbol property - Object.defineProperty(data, sym, { - value: "nonEnumValue", - enumerable: false, - }); +it("should display non-enumerable Symbol properties when showNonenumerable is true", () => { + const sym = Symbol("nonEnumSymbol"); + const data = {}; + // Define a non-enumerable Symbol property + Object.defineProperty(data, sym, { + value: "nonEnumValue", + enumerable: false, + }); - const { container } = render(); + const { container } = render(); - // Check that the non-enumerable Symbol property is displayed - expect(container.textContent).toContain("Symbol(nonEnumSymbol)"); - expect(container.textContent).toContain("nonEnumValue"); - }); + // Check that the non-enumerable Symbol property is displayed + expect(container.textContent).toContain("Symbol(nonEnumSymbol)"); + expect(container.textContent).toContain("nonEnumValue"); +}); - it("should not display non-enumerable Symbol properties when showNonenumerable is false", () => { - const sym = Symbol("nonEnumSymbol"); - const data = {}; - // Define a non-enumerable Symbol property - Object.defineProperty(data, sym, { - value: "nonEnumValue", - enumerable: false, - }); +it("should not display non-enumerable Symbol properties when showNonenumerable is false", () => { + const sym = Symbol("nonEnumSymbol"); + const data = {}; + // Define a non-enumerable Symbol property + Object.defineProperty(data, sym, { + value: "nonEnumValue", + enumerable: false, + }); - const { container } = render(); + const { container } = render(); - // Check that the non-enumerable Symbol property is NOT displayed - expect(container.textContent).not.toContain("Symbol(nonEnumSymbol)"); - expect(container.textContent).not.toContain("nonEnumValue"); - }); - }); + // Check that the non-enumerable Symbol property is NOT displayed + expect(container.textContent).not.toContain("Symbol(nonEnumSymbol)"); + expect(container.textContent).not.toContain("nonEnumValue"); }); diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx index 02e90eef52a..e817cca5903 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx @@ -8,42 +8,6 @@ import { propertyIsEnumerable } from "./utils/object-prototype.js"; import { getPropertyValue } from "./utils/property-utils.js"; const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) => { - // Helper function to handle property iteration for both string and Symbol keys - function* iterateProperties( - data: any, - keys: (string | symbol)[], - formatKey: (key: string | symbol) => string = (k) => - typeof k === "string" ? k || `""` : k.toString(), - ) { - for (const key of keys) { - if (propertyIsEnumerable.call(data, key)) { - const propertyValue = getPropertyValue(data, key); - yield { - name: formatKey(key), - data: propertyValue, - }; - } else if (showNonenumerable) { - // To work around the error (happens some time when propertyName === 'caller' || propertyName === 'arguments') - // 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context - // http://stackoverflow.com/questions/31921189/caller-and-arguments-are-restricted-function-properties-and-cannot-be-access - let propertyValue; - try { - propertyValue = getPropertyValue(data, key); - } catch (e) { - // console.warn(e) - } - - if (propertyValue !== undefined) { - yield { - name: formatKey(key), - data: propertyValue, - isNonenumerable: true, - }; - } - } - } - } - const objectIterator = function* (data: any) { const shouldIterate = (typeof data === "object" && data !== null) || typeof data === "function"; if (!shouldIterate) return; @@ -69,24 +33,47 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = i++; } } else { - // String property names - const keys = Object.getOwnPropertyNames(data); - if (sortObjectKeys === true && !dataIsArray) { + // Get all property keys (both string and Symbol) + const stringKeys = Object.getOwnPropertyNames(data); + const symbolKeys = Object.getOwnPropertySymbols(data); + const allKeys: (string | symbol)[] = [...stringKeys, ...symbolKeys]; + + if (sortObjectKeys && !dataIsArray) { // Array keys should not be sorted in alphabetical order - keys.sort(); - } else if (typeof sortObjectKeys === "function") { - keys.sort(sortObjectKeys); + allKeys.sort((a, b) => { + const aStr = typeof a === "string" ? a : a.toString(); + const bStr = typeof b === "string" ? b : b.toString(); + return aStr.localeCompare(bStr); + }); } - yield* iterateProperties(data, keys); - - // Symbol property names - const symbolKeys = Object.getOwnPropertySymbols(data); - if (sortObjectKeys && !dataIsArray) { - // Sort symbols by their description for consistent display - symbolKeys.sort((a, b) => a.toString().localeCompare(b.toString())); + for (const key of allKeys) { + if (propertyIsEnumerable.call(data, key)) { + const propertyValue = getPropertyValue(data, key); + yield { + name: typeof key === "string" ? key || `""` : key.toString(), + data: propertyValue, + }; + } else if (showNonenumerable) { + // To work around the error (happens some time when propertyName === 'caller' || propertyName === 'arguments') + // 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context + // http://stackoverflow.com/questions/31921189/caller-and-arguments-are-restricted-function-properties-and-cannot-be-access + let propertyValue; + try { + propertyValue = getPropertyValue(data, key); + } catch (e) { + // console.warn(e) + } + + if (propertyValue !== undefined) { + yield { + name: typeof key === "string" ? key : key.toString(), + data: propertyValue, + isNonenumerable: true, + }; + } + } } - yield* iterateProperties(data, symbolKeys); // [[Prototype]] of the object: `Object.getPrototypeOf(data)` // the property name is shown as "__proto__" From 74c0456c32aa2951697ccdcaf0ce9eb04affd440 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:17:17 +0000 Subject: [PATCH 7/8] Add Symbol sorting test and fix empty string key handling consistency Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-inspector.test.tsx | 25 +++++++++++++++++++ .../react/js-inspector/object-inspector.tsx | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx index ad9bfbae7f0..6c7342130e0 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -83,3 +83,28 @@ it("should not display non-enumerable Symbol properties when showNonenumerable i expect(container.textContent).not.toContain("Symbol(nonEnumSymbol)"); expect(container.textContent).not.toContain("nonEnumValue"); }); + +it("should sort Symbol properties when sortObjectKeys is true", () => { + const sym1 = Symbol("zebra"); + const sym2 = Symbol("apple"); + const sym3 = Symbol("middle"); + const data = { + [sym1]: "value1", + [sym2]: "value2", + [sym3]: "value3", + }; + + const { container } = render(); + + const text = container.textContent || ""; + const appleIndex = text.indexOf("Symbol(apple)"); + const middleIndex = text.indexOf("Symbol(middle)"); + const zebraIndex = text.indexOf("Symbol(zebra)"); + + // Symbols should be sorted alphabetically by their description + expect(appleIndex).toBeGreaterThan(-1); + expect(middleIndex).toBeGreaterThan(-1); + expect(zebraIndex).toBeGreaterThan(-1); + expect(appleIndex).toBeLessThan(middleIndex); + expect(middleIndex).toBeLessThan(zebraIndex); +}); diff --git a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx index e817cca5903..e0b4817fea9 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.tsx @@ -67,7 +67,7 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = if (propertyValue !== undefined) { yield { - name: typeof key === "string" ? key : key.toString(), + name: typeof key === "string" ? key || `""` : key.toString(), data: propertyValue, isNonenumerable: true, }; From c64db109e256d0e1110477f92ee64b60d66768cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:04:30 +0000 Subject: [PATCH 8/8] Fix Symbol properties to show in collapsed object preview Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../js-inspector/object-preview.test.tsx | 49 +++++++++++++++++++ .../src/react/js-inspector/object-preview.tsx | 21 +++++--- 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 packages/html-program-viewer/src/react/js-inspector/object-preview.test.tsx diff --git a/packages/html-program-viewer/src/react/js-inspector/object-preview.test.tsx b/packages/html-program-viewer/src/react/js-inspector/object-preview.test.tsx new file mode 100644 index 00000000000..d270a83dd29 --- /dev/null +++ b/packages/html-program-viewer/src/react/js-inspector/object-preview.test.tsx @@ -0,0 +1,49 @@ +import { render } from "@testing-library/react"; +import { expect, it } from "vitest"; +import { ObjectPreview } from "./object-preview.js"; + +it("should display Symbol properties in object preview", () => { + const sym = Symbol("testSymbol"); + const data = { + stringProp: "value1", + [sym]: "symbolValue", + }; + + const { container } = render(); + + // Check that both string and Symbol properties are displayed in preview + expect(container.textContent).toContain("stringProp"); + expect(container.textContent).toContain("value1"); + expect(container.textContent).toContain("Symbol(testSymbol)"); + expect(container.textContent).toContain("symbolValue"); +}); + +it("should display Symbol properties without description in preview", () => { + const sym = Symbol(); + const data = { + [sym]: "value", + }; + + const { container } = render(); + + expect(container.textContent).toContain("Symbol()"); + expect(container.textContent).toContain("value"); +}); + +it("should respect max properties limit including Symbol properties", () => { + const sym1 = Symbol("first"); + const sym2 = Symbol("second"); + const data = { + prop1: "val1", + prop2: "val2", + prop3: "val3", + prop4: "val4", + [sym1]: "symVal1", + [sym2]: "symVal2", + }; + + const { container } = render(); + + // Should show ellipsis when exceeding max properties (5) + expect(container.textContent).toContain("…"); +}); diff --git a/packages/html-program-viewer/src/react/js-inspector/object-preview.tsx b/packages/html-program-viewer/src/react/js-inspector/object-preview.tsx index f5e0a98b6fb..98c81d6b844 100644 --- a/packages/html-program-viewer/src/react/js-inspector/object-preview.tsx +++ b/packages/html-program-viewer/src/react/js-inspector/object-preview.tsx @@ -54,20 +54,29 @@ export const ObjectPreview: FC = ({ data }) => { } else { const maxProperties = OBJECT_MAX_PROPERTIES; const propertyNodes: ReactNode[] = []; - for (const propertyName in object) { - if (hasOwnProperty.call(object, propertyName)) { + + // Get all property keys (both string and Symbol) + const stringKeys = Object.getOwnPropertyNames(object); + const symbolKeys = Object.getOwnPropertySymbols(object); + const allKeys: (string | symbol)[] = [...stringKeys, ...symbolKeys]; + const totalProperties = allKeys.length; + + for (let i = 0; i < allKeys.length; i++) { + const key = allKeys[i]; + if (hasOwnProperty.call(object, key)) { let ellipsis; if ( propertyNodes.length === maxProperties - 1 && - Object.keys(object).length > maxProperties + totalProperties > maxProperties ) { ellipsis = ; } - const propertyValue = getPropertyValue(object, propertyName); + const propertyValue = getPropertyValue(object, key); + const displayName = typeof key === "string" ? key || `""` : key.toString(); propertyNodes.push( - - + + {ellipsis}