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

463 lines
14 KiB
TypeScript

import { test, describe, afterEach, 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 event from "@testing-library/user-event";
import Code from "./Index.svelte";
const default_props = {
value: "print('hello')",
language: "python",
lines: 5,
interactive: true,
show_label: true,
label: "Code"
};
// Code uses BlockLabel (not BlockTitle/block-info) and conditionally renders it
// via {#if show_label}, so the shared show_label:false test (which expects the
// label to remain in the DOM with sr-only) does not apply. Custom label tests
// are written below in the "Props: show_label" describe block.
run_shared_prop_tests({
component: Code,
name: "Code",
base_props: {
value: "print('hello')",
language: "python",
lines: 5
},
has_label: false
});
describe("Props: show_label", () => {
afterEach(() => cleanup());
test("show_label=true renders the label text", async () => {
const { getByText } = await render(Code, {
...default_props,
label: "My Code Editor",
show_label: true
});
expect(getByText("My Code Editor")).toBeVisible();
});
test("show_label=false hides the label from view", async () => {
const { queryByText } = await render(Code, {
...default_props,
label: "My Code Editor",
show_label: false
});
// Code uses {#if show_label} so the BlockLabel is not rendered at all.
expect(queryByText("My Code Editor")).toBeNull();
});
test("label text is derived from the label prop", async () => {
const { getByText } = await render(Code, {
...default_props,
label: "Custom Label Text",
show_label: true
});
expect(getByText("Custom Label Text")).toBeTruthy();
});
});
describe("Code", () => {
afterEach(() => cleanup());
test("renders the code editor when a value is provided", async () => {
const { getByLabelText } = await render(Code, default_props);
expect(getByLabelText("Code input container")).toBeTruthy();
});
test("renders the editor when interactive with no value", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
value: ""
});
expect(getByLabelText("Code input container")).toBeTruthy();
});
test("renders empty state (no editor) when value is empty and not interactive", async () => {
const { queryByLabelText } = await render(Code, {
...default_props,
value: "",
interactive: false
});
expect(queryByLabelText("Code input container")).toBeNull();
});
test("renders the editor (not empty state) when value is present and not interactive", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
interactive: false
});
expect(getByLabelText("Code input container")).toBeTruthy();
});
});
describe("Props: interactive", () => {
afterEach(() => cleanup());
test("interactive=true makes the editor contenteditable", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
interactive: true
});
const editor = getByLabelText("Code input container");
expect(editor).toHaveAttribute("contenteditable", "true");
});
test("interactive=false makes the editor read-only", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
interactive: false
});
const editor = getByLabelText("Code input container");
expect(editor).toHaveAttribute("contenteditable", "false");
});
});
describe("Props: buttons", () => {
afterEach(() => cleanup());
test("default (null) shows both copy and download buttons", async () => {
const { getByLabelText, getByRole } = await render(Code, {
...default_props,
buttons: null
});
expect(getByLabelText("Copy")).toBeTruthy();
expect(getByLabelText("Download")).toBeTruthy();
});
test("buttons=['copy', 'download'] shows both buttons", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
buttons: ["copy", "download"]
});
expect(getByLabelText("Copy")).toBeTruthy();
expect(getByLabelText("Download")).toBeTruthy();
});
test("buttons=['copy'] shows only the copy button", async () => {
const { getByLabelText, queryByLabelText } = await render(Code, {
...default_props,
buttons: ["copy"]
});
expect(getByLabelText("Copy")).toBeTruthy();
expect(queryByLabelText("Download")).toBeNull();
});
test("buttons=['download'] shows only the download button", async () => {
const { getByLabelText, queryByLabelText } = await render(Code, {
...default_props,
buttons: ["download"]
});
expect(getByLabelText("Download")).toBeTruthy();
expect(queryByLabelText("Copy")).toBeNull();
});
test("buttons=[] shows no copy or download buttons", async () => {
const { queryByLabelText } = await render(Code, {
...default_props,
buttons: []
});
expect(queryByLabelText("Copy")).toBeNull();
expect(queryByLabelText("Download")).toBeNull();
});
test("custom button is rendered in the toolbar", async () => {
const { getByRole } = await render(Code, {
...default_props,
buttons: [{ id: 42, value: "Run", icon: null }]
});
expect(getByRole("button", { name: "Run" })).toBeTruthy();
});
test("clicking a custom button fires custom_button_click with the button id", async () => {
const { listen, getByRole } = await render(Code, {
...default_props,
buttons: [{ id: 99, value: "Execute", icon: null }]
});
const custom = listen("custom_button_click");
await fireEvent.click(getByRole("button", { name: "Execute" }));
expect(custom).toHaveBeenCalledTimes(1);
expect(custom).toHaveBeenCalledWith({ id: 99 });
});
});
describe("Props: language", () => {
afterEach(() => cleanup());
const languages = [
"python",
"javascript",
"typescript",
"json",
"html",
"css",
"markdown",
"shell",
"sql",
"yaml",
"dockerfile",
"r",
"c",
"cpp"
] as const;
for (const lang of languages) {
test(`language='${lang}' renders without error`, async () => {
const { getByLabelText } = await render(Code, {
...default_props,
language: lang
});
expect(getByLabelText("Code input container")).toBeTruthy();
});
}
test("language=null renders without error", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
language: null
});
expect(getByLabelText("Code input container")).toBeTruthy();
});
});
test.todo(
"VISUAL: lines=10 sets the minimum visible height of the editor to 10 line-heights — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: max_lines=3 caps the editor height at 3 line-heights and enables scrolling for longer content — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: show_line_numbers=true renders the line number gutter — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: show_line_numbers=false hides the line number gutter — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: wrap_lines=true wraps long lines within the container width instead of scrolling — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: wrap_lines=false lets long lines overflow horizontally — needs Playwright screenshot comparison"
);
test.todo(
"VISUAL: autocomplete=true shows an autocompletion dropdown when typing in supported languages — needs Playwright interaction test"
);
describe("Events", () => {
afterEach(() => cleanup());
test("change: not fired on initial mount", async () => {
const { listen } = await render(Code, default_props);
// retrospective: true replays any events buffered during render
const change = listen("change", { retrospective: true });
expect(change).not.toHaveBeenCalled();
});
test("change: fired when value is updated via set_data", async () => {
const { listen, set_data } = await render(Code, default_props);
const change = listen("change");
await set_data({ value: "print('world')" });
expect(change).toHaveBeenCalledTimes(1);
});
test("change: not fired when the same value is set twice", async () => {
const { listen, set_data } = await render(Code, default_props);
const change = listen("change");
await set_data({ value: "x = 1" });
await set_data({ value: "x = 1" });
expect(change).toHaveBeenCalledTimes(1);
});
test("input: fired when the user types in the editor", async () => {
const { listen, getByLabelText } = await render(Code, default_props);
const input = listen("input");
const editor = getByLabelText("Code input container");
editor.focus();
await event.keyboard("a");
await waitFor(() => expect(input).toHaveBeenCalledTimes(1));
});
test("focus: fired when the editor gains focus", async () => {
const { listen, getByLabelText } = await render(Code, default_props);
const focus = listen("focus");
const editor = getByLabelText("Code input container");
editor.focus();
expect(focus).toHaveBeenCalledTimes(1);
});
test("blur: fired when the editor loses focus", async () => {
const { listen, getByLabelText } = await render(Code, default_props);
const blur = listen("blur");
const editor = getByLabelText("Code input container");
editor.focus();
editor.blur();
expect(blur).toHaveBeenCalledTimes(1);
});
test("custom_button_click: carries the button id in the payload", async () => {
const { listen, getByRole } = await render(Code, {
...default_props,
buttons: [{ id: 7, value: "Go", icon: null }]
});
const custom = listen("custom_button_click");
await fireEvent.click(getByRole("button", { name: "Go" }));
expect(custom).toHaveBeenCalledWith({ id: 7 });
});
});
describe("get_data / set_data", () => {
afterEach(() => cleanup());
test("get_data returns the initial value", async () => {
const { get_data } = await render(Code, default_props);
const data = await get_data();
expect(data.value).toBe("print('hello')");
});
test("set_data updates the value returned by get_data", async () => {
const { get_data, set_data } = await render(Code, default_props);
await set_data({ value: "x = 42" });
const data = await get_data();
expect(data.value).toBe("x = 42");
});
test("set_data / get_data round-trip", async () => {
const { get_data, set_data } = await render(Code, default_props);
const new_value = "def foo():\n return 1";
await set_data({ value: new_value });
const data = await get_data();
expect(data.value).toBe(new_value);
});
test("user typing is reflected in get_data", async () => {
const { get_data, getByLabelText } = await render(Code, {
...default_props,
value: ""
});
const editor = getByLabelText("Code input container");
editor.focus();
await event.keyboard("hi");
await waitFor(async () => {
const data = await get_data();
expect(data.value).toContain("hi");
});
});
});
describe("Copy button", () => {
afterEach(() => cleanup());
test("clicking the copy button writes the code value to the clipboard", async () => {
// navigator.clipboard is unavailable in insecure (non-HTTPS) test origins
// so we mock writeText to prevent the silent no-op in Copy.svelte's `if
// ("clipboard" in navigator)` guard. This mock makes clipboard available.
Object.defineProperty(navigator, "clipboard", {
value: { writeText: vi.fn().mockResolvedValue(undefined) },
writable: true,
configurable: true
});
const { getByLabelText } = await render(Code, {
...default_props,
value: "my code"
});
await fireEvent.click(getByLabelText("Copy"));
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("my code");
});
test.todo(
"VISUAL: copy button icon changes from Copy to Check for ~2 seconds after clicking — needs Playwright screenshot comparison"
);
});
describe("Download button", () => {
afterEach(() => cleanup());
test("download link has filename extension matching the language", async () => {
const { getByRole } = await render(Code, {
...default_props,
language: "python",
value: "print('hello')"
});
// The Download button is inside an <a download="file.py"> anchor
const link = getByRole("link");
expect(link).toHaveAttribute("download", "file.py");
});
test("download link href is a blob URL", async () => {
const { getByRole } = await render(Code, {
...default_props,
value: "some code"
});
const link = getByRole("link");
expect(link.getAttribute("href")).toMatch(/^blob:/);
});
test.todo(
"VISUAL: download button icon changes from Download to Check for ~2 seconds after clicking — needs Playwright screenshot comparison"
);
});
describe("Edge cases", () => {
afterEach(() => cleanup());
test("null value renders without error", async () => {
const { queryByLabelText } = await render(Code, {
...default_props,
value: null
});
// null value with non-interactive should show empty state (no editor)
// but with interactive=true, editor is shown
expect(queryByLabelText("Code input container")).toBeTruthy();
});
test("empty string value with interactive=true shows the editor", async () => {
const { getByLabelText } = await render(Code, {
...default_props,
value: "",
interactive: true
});
expect(getByLabelText("Code input container")).toBeTruthy();
});
// The backend strips `value` from the config when gr.Code has no initial
// value, so the frontend sees undefined rather than null (issue #13598).
test("missing value with interactive=true renders the editor", async () => {
const { value: _, ...props_without_value } = default_props;
const { getByLabelText } = await render(Code, props_without_value);
expect(getByLabelText("Code input container")).toBeVisible();
});
test("value set from the backend after mounting with no value is displayed", async () => {
const { value: _, ...props_without_value } = default_props;
const { getByLabelText, get_data, set_data } = await render(
Code,
props_without_value
);
await set_data({ value: "default value" });
const data = await get_data();
expect(data.value).toBe("default value");
expect(getByLabelText("Code input container")).toHaveTextContent(
"default value"
);
});
test("value with leading and trailing whitespace is displayed", async () => {
const { get_data } = await render(Code, {
...default_props,
value: " hello "
});
const data = await get_data();
expect(data.value).toBe(" hello ");
});
});