Repository: preactjs/preact-devtools (ts)

Issue:
Cannot convert a Symbol value to a string
if a useState hooks hold a ES6 Symbol value, devtools will throw error and cannot work properly.
![image](https://user-images.githubusercontent.com/13177782/81775539-9ab06e00-951f-11ea-9abf-1eab72b0db58.png)
![image](https://user-images.githubusercontent.com/13177782/81775568-a6039980-951f-11ea-9301-d4a1fe2c2eb6.png)



Pull request description:
Fix symbol values not supported
Fixes #193 .

Patch:
diff --git a/src/adapter/10/utils.ts b/src/adapter/10/utils.ts
index 087158a..14ebbd1 100644
--- a/src/adapter/10/utils.ts
+++ b/src/adapter/10/utils.ts
@@ -49,6 +49,12 @@ export function jsonify(
 				name: data.displayName || data.name || "anonymous",
 			};
 		}
+		case "symbol": {
+			return {
+				type: "symbol",
+				name: data.toString(),
+			};
+		}
 		case "object": {
 			if (data === null) return null;
 			else if (data instanceof window.Blob) {
diff --git a/src/view/components/DataInput/parseValue.ts b/src/view/components/DataInput/parseValue.ts
index 2b0c1a6..e578ce0 100644
--- a/src/view/components/DataInput/parseValue.ts
+++ b/src/view/components/DataInput/parseValue.ts
@@ -57,6 +57,7 @@ export function genPreview(v: any): string {
 				return `ƒ ${v.name === "anonymous" ? "" : truncate(v.name)}()`;
 			}
 			if (v.type === "blob") return "Blob {}";
+			if (v.type === "symbol") return v.name;
 		}
 
 		const obj = Object.entries(v).map(x => {
diff --git a/src/view/components/sidebar/inspect/parseProps.ts b/src/view/components/sidebar/inspect/parseProps.ts
index 7ac209b..aaaa847 100644
--- a/src/view/components/sidebar/inspect/parseProps.ts
+++ b/src/view/components/sidebar/inspect/parseProps.ts
@@ -170,6 +170,22 @@ export function parseProps(
 					children: [],
 					meta: null,
 				});
+			} else if (
+				// Same for Symbols
+				maybeCustom &&
+				typeof data.name === "string" &&
+				data.type === "symbol"
+			) {
+				out.set(path, {
+					depth,
+					name,
+					id: path,
+					type: "symbol",
+					editable: false,
+					value: data,
+					children: [],
+					meta: null,
+				});
 			} else {
 				const node: PropData = {
 					depth,


Test patch:
diff --git a/src/adapter/10/utils.test.tsx b/src/adapter/10/utils.test.tsx
index d701053..11b612c 100644
--- a/src/adapter/10/utils.test.tsx
+++ b/src/adapter/10/utils.test.tsx
@@ -117,6 +117,14 @@ describe("jsonify", () => {
 			bar: { foo: 123 },
 		});
 	});
+
+	it("should parse symbols", () => {
+		const data: any = Symbol("foo");
+		expect(jsonify(data, () => null, new Set())).to.deep.equal({
+			type: "symbol",
+			name: "Symbol(foo)",
+		});
+	});
 });
 
 describe("setInCopy", () => {
diff --git a/src/view/components/DataInput/parseValue.test.ts b/src/view/components/DataInput/parseValue.test.ts
index 5e48c08..d4072b7 100644
--- a/src/view/components/DataInput/parseValue.test.ts
+++ b/src/view/components/DataInput/parseValue.test.ts
@@ -66,6 +66,10 @@ describe("genPreview", () => {
 		expect(genPreview([1, 2, { a: 3 }])).to.equal("[1, 2, {a: 3}]");
 		expect(genPreview({ a: 123, b: [1, 2] })).to.equal("{a: 123, b: [1, 2]}");
 
+		expect(genPreview({ type: "symbol", name: "Symbol(foo)" })).to.equal(
+			"Symbol(foo)",
+		);
+
 		// Special values
 		expect(genPreview("__preact_empty__")).to.equal("");
 	});
diff --git a/src/view/components/sidebar/inspect/parseProps.test.ts b/src/view/components/sidebar/inspect/parseProps.test.ts
index d031a5c..ddd5664 100644
--- a/src/view/components/sidebar/inspect/parseProps.test.ts
+++ b/src/view/components/sidebar/inspect/parseProps.test.ts
@@ -297,4 +297,23 @@ describe("parseProps", () => {
 			},
 		]);
 	});
+
+	it("should not mark Symbols as editable", () => {
+		const tree = parseProps({ type: "symbol", name: "Symbol(foo)" }, "", 2);
+		expect(serialize(tree)).to.deep.equal([
+			{
+				editable: false,
+				depth: 0,
+				id: "",
+				name: "",
+				type: "symbol",
+				value: {
+					type: "symbol",
+					name: "Symbol(foo)",
+				},
+				children: [],
+				meta: null,
+			},
+		]);
+	});
 });
diff --git a/test-e2e/tests/fixtures/symbols.js b/test-e2e/tests/fixtures/symbols.js
new file mode 100644
index 0000000..6d9145f
--- /dev/null
+++ b/test-e2e/tests/fixtures/symbols.js
@@ -0,0 +1,43 @@
+const { render, Component } = preact;
+const { useState } = preactHooks;
+
+function SymbolComponent() {
+	const [v] = useState(Symbol("foobar"));
+
+	return html`
+		<div style="padding: 2rem;">
+			${v.toString()}
+		</div>
+	`;
+}
+
+function Child(props) {
+	return html`<div>${props.children.toString()}</div>`;
+}
+
+function PropComponent() {
+	return html`
+		<div style="padding: 2rem;">
+			<${Child}>${Symbol("foobar")}<//>
+		</div>
+	`;
+}
+
+class ClassComponent extends Component {
+	state = {
+		value: Symbol("foobar"),
+	};
+
+	render() {
+		return html`<div>${this.state.value.toString()}</div>`;
+	}
+}
+
+render(
+	html`
+		<${SymbolComponent} />
+		<${PropComponent} />
+		<${ClassComponent} />
+	`,
+	document.getElementById("app"),
+);
diff --git a/test-e2e/tests/symbol-value.test.ts b/test-e2e/tests/symbol-value.test.ts
new file mode 100644
index 0000000..715f437
--- /dev/null
+++ b/test-e2e/tests/symbol-value.test.ts
@@ -0,0 +1,32 @@
+import { expect } from "chai";
+import { newTestPage, clickNestedText } from "../test-utils";
+import { closePage, getText } from "pentf/browser_utils";
+import { wait } from "pentf/utils";
+
+export const description = "Display symbol values";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "symbols");
+
+	await wait(200);
+
+	// Hooks
+	await clickNestedText(devtools, "SymbolComponent");
+	await devtools.waitForSelector('[data-testid="Hooks"]');
+	let text = await getText(devtools, '[data-testid="prop-value"]');
+	expect(text).to.equal("Symbol(foobar)");
+
+	// Props
+	await clickNestedText(devtools, "Child");
+	await devtools.waitForSelector('[data-testid="Props"]');
+	text = await getText(devtools, '[data-testid="prop-value"]');
+	expect(text).to.equal("Symbol(foobar)");
+
+	// State
+	await clickNestedText(devtools, "ClassComponent");
+	await devtools.waitForSelector('[data-testid="State"]');
+	text = await getText(devtools, '[data-testid="prop-value"]');
+	expect(text).to.equal("Symbol(foobar)");
+
+	await closePage(page);
+}