项目文件夹

文件
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

560 行
16 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 Dialogue from "./Index.svelte";
import type { DialogueLine } from "./utils";
// These are backend functions that do not exist when the tests run
// the mocks are neccessary
function mock_server() {
return {
format: vi.fn(async (value: DialogueLine[]): Promise<string> => {
return value.map((line) => `${line.speaker}: ${line.text}`).join("\n");
}),
unformat: vi.fn(
async (params: { text: string }): Promise<DialogueLine[]> => {
const lines = params.text.split("\n").filter((line) => line.trim());
return lines.map((line) => {
const colonIndex = line.indexOf(":");
if (colonIndex > -1) {
return {
speaker: line.slice(0, colonIndex).trim(),
text: line.slice(colonIndex + 1).trim()
};
}
return { speaker: "", text: line.trim() };
});
}
)
};
}
const default_props = {
label: "Dialogue",
show_label: true,
interactive: true,
value: [{ speaker: "Speaker1", text: "Hello" }],
speakers: ["Speaker1", "Speaker2"],
tags: ["tag1", "tag2", "tag3"],
ui_mode: "both" as const,
max_lines: undefined as number | undefined,
buttons: ["copy"],
placeholder: "Enter text...",
submit_btn: false,
color_map: undefined as Record<string, string> | undefined,
separator: "\n",
info: "",
server: mock_server()
};
run_shared_prop_tests({
component: Dialogue,
name: "Dialogue",
base_props: {
value: [{ speaker: "Speaker1", text: "Hello" }],
speakers: ["Speaker1", "Speaker2"],
tags: ["tag1", "tag2"],
ui_mode: "both",
interactive: true,
server: mock_server()
}
});
describe("Dialogue", () => {
afterEach(() => cleanup());
test("renders with dialogue lines from value", async () => {
const { getByDisplayValue } = await render(Dialogue, {
...default_props,
value: [
{ speaker: "Speaker1", text: "Hello world" },
{ speaker: "Speaker2", text: "How are you?" }
]
});
expect(getByDisplayValue("Hello world")).toBeInTheDocument();
expect(getByDisplayValue("How are you?")).toBeInTheDocument();
});
test("renders empty line when value is empty array and speakers exist", async () => {
const { getAllByRole } = await render(Dialogue, {
...default_props,
value: [],
speakers: ["Speaker1"]
});
// An empty line is automatically added
const textareas = getAllByRole("textbox");
expect(textareas.length).toEqual(1);
});
});
describe("Props: ui_mode", () => {
afterEach(() => cleanup());
test("ui_mode='dialogue' shows only dialogue lines view", async () => {
const { queryByTestId, queryByRole, getByDisplayValue } = await render(
Dialogue,
{
...default_props,
ui_mode: "dialogue",
value: [{ speaker: "Speaker1", text: "Hello" }]
}
);
expect(getByDisplayValue("Hello")).toBeInTheDocument();
expect(queryByTestId("textbox")).not.toBeInTheDocument();
expect(queryByRole("switch")).not.toBeInTheDocument();
});
test("ui_mode='text' shows only plain text view", async () => {
const { getByTestId, queryByRole } = await render(Dialogue, {
...default_props,
ui_mode: "text",
value: "Plain text value"
});
expect(getByTestId("textbox")).toBeInTheDocument();
expect(queryByRole("switch")).not.toBeInTheDocument();
});
test("ui_mode='both' shows dialogue view with switch to toggle", async () => {
const { getByDisplayValue, getByRole } = await render(Dialogue, {
...default_props,
ui_mode: "both",
value: [{ speaker: "Speaker1", text: "Hello" }]
});
expect(getByDisplayValue("Hello")).toBeInTheDocument();
expect(getByRole("switch")).toBeVisible();
});
});
describe("Props: interactive", () => {
afterEach(() => cleanup());
test("interactive=false disables textareas", async () => {
const { getByDisplayValue } = await render(Dialogue, {
...default_props,
interactive: false,
value: [{ speaker: "Speaker1", text: "Hello" }]
});
const textarea = getByDisplayValue("Hello") as HTMLTextAreaElement;
expect(textarea).toBeDisabled();
});
test("interactive=true enables textareas", async () => {
const { getByDisplayValue } = await render(Dialogue, {
...default_props,
interactive: true,
value: [{ speaker: "Speaker1", text: "Hello" }]
});
const textarea = getByDisplayValue("Hello") as HTMLTextAreaElement;
expect(textarea).toBeEnabled();
});
test("interactive=false hides add/delete buttons", async () => {
const { queryByRole } = await render(Dialogue, {
...default_props,
interactive: false,
value: [
{ speaker: "Speaker1", text: "Line1" },
{ speaker: "Speaker2", text: "Line2" }
]
});
// When interactive=false, the buttons should not be visible in the DOM
// because the parent container has class:hidden which removes them from layout
const addButton = queryByRole("button", { name: "Add new line" });
const deleteButton = queryByRole("button", { name: "Remove current line" });
expect(addButton).not.toBeInTheDocument();
expect(deleteButton).not.toBeInTheDocument();
});
});
describe("Props: placeholder", () => {
afterEach(() => cleanup());
test("placeholder is shown in textarea", async () => {
const { getByPlaceholderText } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "" }],
placeholder: "Type your message here..."
});
expect(
getByPlaceholderText("Type your message here...")
).toBeInTheDocument();
});
});
describe("Props: submit_btn", () => {
afterEach(() => cleanup());
test("submit_btn=false does not render submit button", async () => {
const { queryByTestId } = await render(Dialogue, {
...default_props,
submit_btn: false
});
const submitButton = queryByTestId("dialogue-submit-button");
expect(submitButton).not.toBeInTheDocument();
});
test("submit_btn=true renders default submit button", async () => {
const { getByTestId } = await render(Dialogue, {
...default_props,
submit_btn: true
});
const submitButton = getByTestId("dialogue-submit-button");
expect(submitButton).toBeInTheDocument();
});
test("submit_btn='Custom Text' renders submit button with custom text", async () => {
const { getByRole } = await render(Dialogue, {
...default_props,
submit_btn: "Send Message"
});
const submitButton = getByRole("button", { name: "Send Message" });
expect(submitButton).toBeInTheDocument();
});
});
describe("Props: buttons", () => {
afterEach(() => cleanup());
test("buttons=['copy'] shows copy button", async () => {
const { getByLabelText } = await render(Dialogue, {
...default_props,
buttons: ["copy"]
});
expect(getByLabelText("Copy")).toBeInTheDocument();
});
test("custom buttons trigger custom_button_click event", async () => {
const { getByLabelText, listen } = await render(Dialogue, {
...default_props,
buttons: [{ value: "custom", id: 42, icon: null }]
});
const customClick = listen("custom_button_click");
const customButton = getByLabelText("custom");
await fireEvent.click(customButton);
expect(customClick).toHaveBeenCalledTimes(1);
expect(customClick).toHaveBeenCalledWith({ id: 42 });
});
});
describe("Props: max_lines", () => {
afterEach(() => cleanup());
test("add button is hidden when max_lines is reached", async () => {
const { queryAllByTestId } = await render(Dialogue, {
...default_props,
max_lines: 2,
value: [
{ speaker: "Speaker1", text: "Line1" },
{ speaker: "Speaker2", text: "Line2" }
]
});
const addButtons = queryAllByTestId(/^dialogue-add-button-/);
expect(addButtons.length).toBe(1);
});
});
describe("Props: tags", () => {
afterEach(() => cleanup());
test("typing ':' shows tag autocomplete menu with matching options", async () => {
const { getByDisplayValue, queryByTestId } = await render(Dialogue, {
...default_props,
tags: ["greeting", "farewell", "question"],
value: [{ speaker: "Speaker1", text: "" }]
});
const textarea = getByDisplayValue("") as HTMLTextAreaElement;
textarea.focus();
await event.type(textarea, "Hello :");
expect(queryByTestId("dialogue-tag-menu")).toBeInTheDocument();
});
});
describe("Events", () => {
afterEach(() => cleanup());
test("input event is dispatched when text is entered", async () => {
const { getByDisplayValue, listen } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "" }]
});
const inputEvent = listen("input");
const textarea = getByDisplayValue("") as HTMLTextAreaElement;
textarea.focus();
await event.type(textarea, "abc");
expect(inputEvent).toHaveBeenCalled();
});
test("change event is dispatched when value changes from outside", async () => {
const { listen, set_data } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "Initial" }]
});
const changeEvent = listen("change");
await set_data({ value: [{ speaker: "Speaker1", text: "Updated" }] });
expect(changeEvent).toHaveBeenCalledTimes(1);
});
test("submit event is dispatched when submit button is clicked", async () => {
const { getByTestId, listen } = await render(Dialogue, {
...default_props,
submit_btn: true
});
const submitEvent = listen("submit");
const submitButton = getByTestId("dialogue-submit-button");
await fireEvent.click(submitButton);
expect(submitEvent).toHaveBeenCalledTimes(1);
});
test("no spurious change event on mount", async () => {
const { listen } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "Hello" }]
});
const changeEvent = listen("change", { retrospective: true });
expect(changeEvent).not.toHaveBeenCalled();
});
});
describe("get_data / set_data", () => {
afterEach(() => cleanup());
test("set_data updates the dialogue lines", async () => {
const { getByDisplayValue, set_data } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "Hello world" }]
});
const initialTextarea = getByDisplayValue("Hello world");
expect(initialTextarea).toBeInTheDocument();
await set_data({ value: [{ speaker: "Speaker1", text: "Updated value" }] });
const updatedTextarea = getByDisplayValue("Updated value");
expect(updatedTextarea).toBeInTheDocument();
expect(() => getByDisplayValue("Hello world")).toThrow();
});
test("set_data with string value switches to plain text mode", async () => {
const { getByTestId, set_data } = await render(Dialogue, {
...default_props,
ui_mode: "both",
value: [{ speaker: "Speaker1", text: "Dialogue mode" }]
});
await set_data({ value: "Plain text mode value" });
const textbox = getByTestId("textbox") as HTMLTextAreaElement;
expect(textbox.value).toBe("Plain text mode value");
});
test("get_data returns current dialogue value", async () => {
const { get_data } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "Hello" }]
});
await vi.waitFor(async () => {
const data = await get_data();
expect(data.value).toEqual([{ speaker: "Speaker1", text: "Hello" }]);
});
});
test("get_data reflects user input changes", async () => {
const { get_data, getByDisplayValue } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "" }]
});
const textarea = getByDisplayValue("") as HTMLTextAreaElement;
textarea.focus();
await event.type(textarea, "User typed text");
const data = await get_data();
expect(data.value).toBeDefined();
expect(data.value[0]).toBeDefined();
expect(data.value[0].text).toBe("User typed text");
});
});
describe("Interactive features", () => {
afterEach(() => cleanup());
test("clicking add button creates a new dialogue line", async () => {
const { getByTestId, getByDisplayValue } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "First line" }]
});
const addButton = getByTestId("dialogue-add-button-0");
await fireEvent.click(addButton);
expect(getByDisplayValue("First line")).toBeInTheDocument();
expect(getByTestId("dialogue-add-button-1")).toBeInTheDocument();
});
test("clicking delete button removes a dialogue line", async () => {
const { getByTestId, getByDisplayValue, queryByDisplayValue } =
await render(Dialogue, {
...default_props,
value: [
{ speaker: "Speaker1", text: "Line 1" },
{ speaker: "Speaker2", text: "Line 2" }
]
});
expect(getByDisplayValue("Line 1")).toBeInTheDocument();
expect(getByDisplayValue("Line 2")).toBeInTheDocument();
const deleteButton = getByTestId("dialogue-delete-button-1");
await fireEvent.click(deleteButton);
expect(getByDisplayValue("Line 1")).toBeInTheDocument();
expect(queryByDisplayValue("Line 2")).not.toBeInTheDocument();
});
});
describe("Mode switching", () => {
afterEach(() => cleanup());
test("mock server format/unformat round-trip works correctly", async () => {
const serverMock = mock_server();
const { getByTestId, getByDisplayValue, getByRole } = await render(
Dialogue,
{
...default_props,
ui_mode: "both",
value: [
{ speaker: "Speaker1", text: "Hello world" },
{ speaker: "Speaker2", text: "How are you?" }
],
server: serverMock
}
);
const switchButton = getByRole("switch");
await fireEvent.click(switchButton);
expect(serverMock.format).toHaveBeenCalled();
expect(serverMock.format).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
speaker: "Speaker1",
text: "Hello world"
}),
expect.objectContaining({
speaker: "Speaker2",
text: "How are you?"
})
])
);
const textbox = getByTestId("textbox") as HTMLTextAreaElement;
expect(textbox.value).toBe("Speaker1: Hello world\nSpeaker2: How are you?");
await fireEvent.click(switchButton);
expect(serverMock.unformat).toHaveBeenCalled();
expect(serverMock.unformat).toHaveBeenCalledWith(
expect.objectContaining({
text: "Speaker1: Hello world\nSpeaker2: How are you?"
})
);
expect(getByDisplayValue("Hello world")).toBeInTheDocument();
expect(getByDisplayValue("How are you?")).toBeInTheDocument();
});
});
describe("Edge cases", () => {
afterEach(() => cleanup());
test("handles undefined max_lines", async () => {
const { queryAllByTestId } = await render(Dialogue, {
...default_props,
max_lines: undefined,
value: [
{ speaker: "Speaker1", text: "Line 1" },
{ speaker: "Speaker2", text: "Line 2" },
{ speaker: "Speaker1", text: "Line 3" }
]
});
const addButtons = queryAllByTestId(/^dialogue-add-button-/);
expect(addButtons.length).toBe(3);
});
test("event deduplication: change not fired for identical value", async () => {
const { listen, set_data } = await render(Dialogue, {
...default_props,
value: [{ speaker: "Speaker1", text: "Hello" }]
});
const changeEvent = listen("change", { retrospective: true });
await set_data({ value: [{ speaker: "Speaker1", text: "Hello" }] });
expect(changeEvent).not.toHaveBeenCalled();
});
});
describe("Plain text mode", () => {
afterEach(() => cleanup());
test("switching to plain text mode shows textarea", async () => {
const { getByTestId, getByRole } = await render(Dialogue, {
...default_props,
ui_mode: "both",
value: [{ speaker: "Speaker1", text: "Dialogue text" }]
});
await fireEvent.click(getByRole("switch"));
expect(getByTestId("textbox")).toBeInTheDocument();
});
test("plain text mode with string initial value", async () => {
const { getByTestId } = await render(Dialogue, {
...default_props,
ui_mode: "text",
value: "Initial plain text"
});
const textbox = getByTestId("textbox") as HTMLTextAreaElement;
expect(textbox.value).toBe("Initial plain text");
});
});
test.todo(
"VISUAL: color_map prop applies custom colors to speaker backgrounds — needs Playwright visual regression screenshot comparison"
);
test.todo(
"VISUAL: default speaker colors are applied when color_map is not provided — needs Playwright visual regression screenshot comparison"
);