Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/html-program-viewer"
---

Fix type graph viewer to display Symbol-keyed decorator state
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);
});
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 @@ -4,7 +4,7 @@ import { JsValue } from "./js-value/js-value.js";
import { ObjectName } from "./object-name.js";

import style from "./object-inspector.module.css";
import { hasOwnProperty } from "./utils/object-prototype.js";
import { hasOwnProperty, propertyIsEnumerable } from "./utils/object-prototype.js";
import { getPropertyValue } from "./utils/property-utils.js";

/* intersperse arr with separator */
Expand Down Expand Up @@ -54,27 +54,36 @@ 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)) {
let ellipsis;
if (
propertyNodes.length === maxProperties - 1 &&
Object.keys(object).length > maxProperties
) {
ellipsis = <span key={"ellipsis"}>…</span>;
}

const propertyValue = getPropertyValue(object, propertyName);
propertyNodes.push(
<span key={propertyName}>
<ObjectName name={propertyName || `""`} />
:&nbsp;
<JsValue value={propertyValue} />
{ellipsis}
</span>,
);
if (ellipsis) break;

// Get all property keys (both string and Symbol), filtering for enumerable ones
const stringKeys = Object.keys(object); // Object.keys only returns enumerable string properties
const symbolKeys = Object.getOwnPropertySymbols(object).filter(sym =>
propertyIsEnumerable.call(object, sym)
);
const allKeys: (string | symbol)[] = [...stringKeys, ...symbolKeys];
const totalProperties = allKeys.length;

for (let i = 0; i < allKeys.length; i++) {
const key = allKeys[i];
let ellipsis;
if (
propertyNodes.length === maxProperties - 1 &&
totalProperties > maxProperties
) {
ellipsis = <span key={"ellipsis"}>…</span>;
}

const propertyValue = getPropertyValue(object, key);
const displayName = typeof key === "string" ? key || `""` : key.toString();
propertyNodes.push(
<span key={typeof key === "string" ? key : `symbol-${i}`}>
<ObjectName name={displayName} />
:&nbsp;
<JsValue value={propertyValue} />
{ellipsis}
</span>,
);
if (ellipsis) break;
}

const objectConstructorName = object.constructor ? object.constructor.name : "Object";
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