Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -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(<ObjectInspector data={data} />);

// 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(<ObjectInspector data={data} />);

// 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(<ObjectInspector data={data} />);

// 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(<ObjectInspector data={data} showNonenumerable={true} />);

// 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(<ObjectInspector data={data} showNonenumerable={false} />);

// 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(<ObjectInspector data={data} sortObjectKeys={true} />);

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);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This is a good start but it doesn't show up in the collapsed object preview, only when expanded, why is that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c64db10. The issue was that object-preview.tsx was using a for...in loop which only iterates over enumerable string properties. Updated it to use Object.getOwnPropertyNames() and Object.getOwnPropertySymbols() to include Symbol keys, matching the approach in object-inspector.tsx. Symbol properties now appear in both collapsed and expanded views.

Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<ObjectPreview data={data} />);

// 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(<ObjectPreview data={data} />);

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(<ObjectPreview data={data} />);

// Should show ellipsis when exceeding max properties (5)
expect(container.textContent).toContain("…");
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,29 @@ export const ObjectPreview: FC<any> = ({ 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 = <span key={"ellipsis"}>…</span>;
}

const propertyValue = getPropertyValue(object, propertyName);
const propertyValue = getPropertyValue(object, key);
const displayName = typeof key === "string" ? key || `""` : key.toString();
propertyNodes.push(
<span key={propertyName}>
<ObjectName name={propertyName || `""`} />
<span key={typeof key === "string" ? key : `symbol-${i}`}>
<ObjectName name={displayName} />
:&nbsp;
<JsValue value={propertyValue} />
{ellipsis}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading