Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

520 lines
16 KiB
TypeScript

import { test, describe, afterEach, beforeEach, expect, vi } from "vitest";
import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
import JSONOutput from "./Index.svelte";
const default_props = {
value: { key: "value" },
label: "JSON Output",
show_label: true,
open: false,
show_indices: false
};
// has_label: false because the shared show_label test looks for [data-testid='block-info']
// which doesn't exist in JSON's BlockLabel structure. JSON uses BlockLabel which applies
// class:hide (display:none) when show_label=false — custom label tests below cover this.
run_shared_prop_tests({
component: JSONOutput,
name: "JSON",
base_props: {
value: { key: "value" },
label: "JSON Output"
},
has_label: false
});
// Custom label tests to replace the skipped shared label tests
describe("Props: label", () => {
afterEach(() => cleanup());
test("label text is rendered", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
label: "My JSON"
});
expect(getByText("My JSON")).toBeInTheDocument();
});
test("show_label=true makes the label visible", async () => {
const { getByTestId } = await render(JSONOutput, {
...default_props,
show_label: true,
label: "My JSON"
});
expect(getByTestId("block-label")).toBeVisible();
});
test("show_label=false hides the label but keeps it in the DOM", async () => {
const { getByTestId } = await render(JSONOutput, {
...default_props,
show_label: false,
label: "My JSON"
});
const label = getByTestId("block-label");
expect(label).toBeInTheDocument();
expect(label).not.toBeVisible();
});
});
describe("JSON", () => {
afterEach(() => cleanup());
test("renders an object value", async () => {
const { getByText } = await render(JSONOutput, default_props);
expect(getByText('"key"')).toBeVisible();
expect(getByText('"value"')).toBeVisible();
});
test("renders an array value", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: [1, 2, 3]
});
expect(getByText("1")).toBeVisible();
expect(getByText("2")).toBeVisible();
expect(getByText("3")).toBeVisible();
});
test("renders a primitive string value", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: "hello"
});
expect(getByText('"hello"')).toBeVisible();
});
test("renders a primitive number value", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: 42
});
expect(getByText("42")).toBeVisible();
});
test("renders boolean true", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: true
});
expect(getByText("true")).toBeVisible();
});
test("renders boolean false inside an object", async () => {
// Top-level value=false is falsy → shows empty state; wrap it to test rendering
const { getByText } = await render(JSONOutput, {
...default_props,
value: { flag: false }
});
expect(getByText("false")).toBeVisible();
});
test("renders null inside an object as the text 'null'", async () => {
// Top-level null shows empty state, so use an object containing null
const { getByText } = await render(JSONOutput, {
...default_props,
value: { a: null }
});
expect(getByText("null")).toBeVisible();
});
test("renders nested objects", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: { outer: { inner: "deep" } }
});
expect(getByText('"outer"')).toBeVisible();
expect(getByText('"inner"')).toBeVisible();
expect(getByText('"deep"')).toBeVisible();
});
test("renders deeply nested structures without crashing", async () => {
const deep = { a: { b: { c: { d: { e: "bottom" } } } } };
const { getByText } = await render(JSONOutput, {
...default_props,
value: deep
});
expect(getByText('"a"')).toBeVisible();
});
});
describe("Empty state", () => {
afterEach(() => cleanup());
test("value=null shows Empty state: no copy button, no content", async () => {
const { queryByLabelText, queryByText } = await render(JSONOutput, {
...default_props,
value: null
});
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
expect(queryByText('"key"')).not.toBeInTheDocument();
});
test("value='' shows Empty state", async () => {
const { queryByLabelText } = await render(JSONOutput, {
...default_props,
value: ""
});
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
});
test("value={} shows Empty state", async () => {
const { queryByLabelText } = await render(JSONOutput, {
...default_props,
value: {}
});
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
});
test("value=[] shows content (empty array is not empty state)", async () => {
const { getByLabelText } = await render(JSONOutput, {
...default_props,
value: []
});
// Copy button present means the content branch is rendered
expect(getByLabelText("Copy")).toBeVisible();
});
test("value with content shows copy button", async () => {
const { getByLabelText } = await render(JSONOutput, default_props);
expect(getByLabelText("Copy")).toBeVisible();
});
});
describe("Props: open", () => {
afterEach(() => cleanup());
test("open=false (default): shallow nodes start expanded, no preview text", async () => {
const { queryByText } = await render(JSONOutput, {
...default_props,
value: { outer: { inner: "deep" } },
open: false
});
// No nodes are collapsed at depth 0-2, so no Object(...) preview should appear
expect(queryByText(/Object\(\d+\)/)).not.toBeInTheDocument();
});
test("open=false: nodes at depth >= 3 auto-collapse and show preview", async () => {
// 4-level nesting: root(0) > a(1) > b(2) > c(3) — depth 3 collapses
const value = { a: { b: { c: { d: "deep" } } } };
const { getByText } = await render(JSONOutput, {
...default_props,
value,
open: false
});
expect(getByText(/Object\(\d+\)/)).toBeVisible();
});
test("open=true: all nodes start expanded regardless of depth", async () => {
const value = { a: { b: { c: { d: "deep" } } } };
const { queryByText, getByText } = await render(JSONOutput, {
...default_props,
value,
open: true
});
// Leaf value visible means nothing was auto-collapsed
expect(getByText('"deep"')).toBeVisible();
expect(queryByText(/Object\(\d+\)/)).not.toBeInTheDocument();
});
});
describe("Props: show_indices", () => {
afterEach(() => cleanup());
const array_value = [10, 20, 30];
test("show_indices=false (default): array indices not rendered as keys", async () => {
const { queryByText } = await render(JSONOutput, {
...default_props,
value: array_value,
show_indices: false
});
expect(queryByText('"0"')).not.toBeInTheDocument();
expect(queryByText('"1"')).not.toBeInTheDocument();
});
test("show_indices=true: array indices rendered as keys", async () => {
const { getByText } = await render(JSONOutput, {
...default_props,
value: array_value,
show_indices: true
});
expect(getByText('"0"')).toBeVisible();
expect(getByText('"1"')).toBeVisible();
expect(getByText('"2"')).toBeVisible();
});
});
describe("Props: buttons", () => {
afterEach(() => cleanup());
test("buttons=null (default): copy button is shown", async () => {
const { getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: null as any
});
expect(getByLabelText("Copy")).toBeVisible();
});
test('buttons=["copy"]: copy button is shown', async () => {
const { getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: ["copy"]
});
expect(getByLabelText("Copy")).toBeVisible();
});
test("buttons=[]: copy button is not shown", async () => {
const { queryByLabelText } = await render(JSONOutput, {
...default_props,
buttons: []
});
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
});
test("buttons=[custom]: copy button absent, custom button shown", async () => {
const { queryByLabelText, getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: [{ value: "Run", id: 1, icon: null }]
});
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
expect(getByLabelText("Run")).toBeVisible();
});
test('buttons=["copy", custom]: both copy and custom button shown', async () => {
const { getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: ["copy", { value: "Analyze", id: 2, icon: null }]
});
expect(getByLabelText("Copy")).toBeVisible();
expect(getByLabelText("Analyze")).toBeVisible();
});
});
describe("Copy button", () => {
// navigator.clipboard.writeText requires HTTPS/permissions in a real browser.
// Mock it so these tests are deterministic regardless of browser security policy.
let write_text: ReturnType<typeof vi.fn>;
beforeEach(() => {
write_text = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText: write_text },
configurable: true,
writable: true
});
});
afterEach(() => cleanup());
test("copy button label is 'Copy' before click", async () => {
const { getByLabelText } = await render(JSONOutput, default_props);
expect(getByLabelText("Copy")).toBeVisible();
});
test("clicking copy calls clipboard.writeText with pretty-printed JSON", async () => {
const value = { name: "test", count: 3 };
const { getByLabelText } = await render(JSONOutput, {
...default_props,
value
});
await fireEvent.click(getByLabelText("Copy"));
await waitFor(() => {
expect(write_text).toHaveBeenCalledTimes(1);
expect(write_text).toHaveBeenCalledWith(JSON.stringify(value, null, 2));
});
});
test("copy button label changes to 'Copied' after click", async () => {
const { getByLabelText } = await render(JSONOutput, default_props);
await fireEvent.click(getByLabelText("Copy"));
await waitFor(() => {
expect(getByLabelText("Copied")).toBeVisible();
});
});
});
describe("Custom buttons", () => {
afterEach(() => cleanup());
test("custom button is rendered with its label", async () => {
const { getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: [{ value: "Run", id: 42, icon: null }]
});
expect(getByLabelText("Run")).toBeVisible();
});
test("clicking custom button fires custom_button_click with correct id", async () => {
const { listen, getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: [{ value: "Run", id: 42, icon: null }]
});
const custom = listen("custom_button_click");
await fireEvent.click(getByLabelText("Run"));
expect(custom).toHaveBeenCalledTimes(1);
expect(custom).toHaveBeenCalledWith({ id: 42 });
});
});
describe("Collapse / expand", () => {
afterEach(() => cleanup());
test("expandable object node has toggle button with aria-label 'Collapse'", async () => {
const { getAllByRole } = await render(JSONOutput, {
...default_props,
value: { name: "Alice", age: 30 }
});
const toggles = getAllByRole("button", { name: "Collapse" });
expect(toggles.length).toBeGreaterThan(0);
});
test("clicking toggle collapses the node and shows Object(N) preview", async () => {
const { getAllByRole, getByText, queryByText } = await render(JSONOutput, {
...default_props,
value: { name: "Alice", age: 30, city: "NYC" }
});
const collapse_btn = getAllByRole("button", { name: "Collapse" })[0];
await fireEvent.click(collapse_btn);
expect(getByText("Object(3)")).toBeVisible();
expect(queryByText('"name"')).not.toBeVisible();
});
test("clicking toggle again expands the node and hides preview", async () => {
const { getAllByRole, getByText, queryByText } = await render(JSONOutput, {
...default_props,
value: { name: "Alice", age: 30, city: "NYC" }
});
// Collapse
const collapse_btn = getAllByRole("button", { name: "Collapse" })[0];
await fireEvent.click(collapse_btn);
// Expand
const expand_btn = getAllByRole("button", { name: "Expand" })[0];
await fireEvent.click(expand_btn);
expect(getByText('"name"')).toBeVisible();
expect(queryByText("Object(3)")).not.toBeInTheDocument();
});
test("collapsed array node shows 'Array(N)' preview", async () => {
const { getAllByRole, getByText } = await render(JSONOutput, {
...default_props,
value: [1, 2, 3, 4]
});
const collapse_btn = getAllByRole("button", { name: "Collapse" })[0];
await fireEvent.click(collapse_btn);
expect(getByText("Array(4)")).toBeVisible();
});
test("clicking preview text expands a collapsed node", async () => {
const { getAllByRole, getByText } = await render(JSONOutput, {
...default_props,
value: { name: "Alice", age: 30 }
});
// Collapse first
const collapse_btn = getAllByRole("button", { name: "Collapse" })[0];
await fireEvent.click(collapse_btn);
// Click the preview button to expand
const preview = getByText(/Object\(\d+\)/);
await fireEvent.click(preview);
expect(getByText('"name"')).toBeVisible();
});
test("primitive value has no toggle button", async () => {
const { queryByRole } = await render(JSONOutput, {
...default_props,
value: "just a string"
});
expect(queryByRole("button", { name: "Collapse" })).not.toBeInTheDocument();
expect(queryByRole("button", { name: "Expand" })).not.toBeInTheDocument();
});
test.todo(
"VISUAL: interactive=false disables toggle buttons — test via BaseJSON with interactive=false prop directly; Index.svelte does not plumb interactive through to JSONNode"
);
});
describe("Events", () => {
afterEach(() => cleanup());
test("change: fired when value updates via set_data", async () => {
const { listen, set_data } = await render(JSONOutput, default_props);
const change = listen("change");
await set_data({ value: { updated: true } });
expect(change).toHaveBeenCalledTimes(1);
});
test("change: not fired again without a value update", async () => {
const { listen } = await render(JSONOutput, default_props);
// Non-retrospective: only captures events fired AFTER this point.
// Verifies no spurious change fires after mount without a data change.
const change = listen("change");
expect(change).not.toHaveBeenCalled();
});
test("change: same primitive value set twice fires only once", async () => {
const { listen, set_data } = await render(JSONOutput, {
...default_props,
value: { initial: true }
});
const change = listen("change");
await set_data({ value: "hello" });
await set_data({ value: "hello" });
expect(change).toHaveBeenCalledTimes(1);
});
test("custom_button_click: fired with { id } when custom button clicked", async () => {
const { listen, getByLabelText } = await render(JSONOutput, {
...default_props,
buttons: [{ value: "Action", id: 7, icon: null }]
});
const custom = listen("custom_button_click");
await fireEvent.click(getByLabelText("Action"));
expect(custom).toHaveBeenCalledTimes(1);
expect(custom).toHaveBeenCalledWith({ id: 7 });
});
});
describe("get_data / set_data", () => {
afterEach(() => cleanup());
test("set_data with new object renders new content", async () => {
const { set_data, getByText } = await render(JSONOutput, default_props);
await set_data({ value: { foo: "bar" } });
expect(getByText('"foo"')).toBeVisible();
expect(getByText('"bar"')).toBeVisible();
});
test("set_data with null switches to Empty state", async () => {
const { set_data, queryByLabelText } = await render(
JSONOutput,
default_props
);
await set_data({ value: null });
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
});
test("set_data with empty object switches to Empty state", async () => {
const { set_data, queryByLabelText } = await render(
JSONOutput,
default_props
);
await set_data({ value: {} });
expect(queryByLabelText("Copy")).not.toBeInTheDocument();
});
test("set_data then get_data round-trip preserves value", async () => {
const { set_data, get_data } = await render(JSONOutput, default_props);
const test_value = { x: 42, y: "hello", z: [1, 2, 3] };
await set_data({ value: test_value });
const data = await get_data();
expect(data.value).toEqual(test_value);
});
});
test.todo(
"VISUAL: theme_mode='dark' applies dark-mode CSS variables (--bracket-color, --number-color) to JSONNode — needs Playwright visual regression screenshot comparison"
);
test.todo(
"VISUAL: theme_mode='light' applies light-mode CSS colors to JSONNode — needs Playwright visual regression screenshot comparison"
);