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..6c7342130e0 --- /dev/null +++ b/packages/html-program-viewer/src/react/js-inspector/object-inspector.test.tsx @@ -0,0 +1,110 @@ +import { render } from "@testing-library/react"; +import { expect, it } from "vitest"; +import { ObjectInspector } from "./object-inspector.js"; + +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 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"); +}); + +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 b5bf0f4af58..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 @@ -33,19 +33,25 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = i++; } } else { - 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); + }); } - for (const propertyName of keys) { - if (propertyIsEnumerable.call(data, propertyName)) { - const propertyValue = getPropertyValue(data, propertyName); + for (const key of allKeys) { + if (propertyIsEnumerable.call(data, key)) { + const propertyValue = getPropertyValue(data, key); yield { - name: propertyName || `""`, + name: typeof key === "string" ? key || `""` : key.toString(), data: propertyValue, }; } else if (showNonenumerable) { @@ -54,14 +60,14 @@ const createIterator = (showNonenumerable?: boolean, sortObjectKeys?: boolean) = // http://stackoverflow.com/questions/31921189/caller-and-arguments-are-restricted-function-properties-and-cannot-be-access let propertyValue; try { - propertyValue = getPropertyValue(data, propertyName); + propertyValue = getPropertyValue(data, key); } catch (e) { // console.warn(e) } if (propertyValue !== undefined) { yield { - name: propertyName, + name: typeof key === "string" ? key || `""` : key.toString(), data: propertyValue, isNonenumerable: true, }; 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} 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 {