Back to Repositories

Testing Chatbot Component Functionality in Gradio

This test suite validates the Chatbot component functionality in Gradio, covering message rendering, media handling, and user interactions. It ensures proper display and behavior of chat messages across different content types and states.

Test Coverage Overview

The test suite provides comprehensive coverage of the Chatbot component’s core features:

  • Message rendering for user and bot interactions
  • Handling of null and empty message states
  • Dynamic message updates and additions
  • Multi-media content support (images, video, audio)
  • File attachment handling and hyperlink generation
  • Clipboard integration for message copying

Implementation Analysis

The testing approach utilizes Vitest and testing-library with a focus on component isolation. Tests employ render and fireEvent utilities for DOM interaction verification, while leveraging TypeScript types for robust test data structures. Mock implementations handle external dependencies like clipboard operations.

Testing patterns include component mounting, state updates, and event handling verification using Jest-style assertions.

Technical Details

Testing tools and configuration:

  • Vitest test runner with Jest-compatible API
  • @testing-library/svelte for component rendering
  • Custom test utilities from @self/tootils
  • TypeScript for type safety
  • Mock implementations for browser APIs
  • Cleanup hooks for test isolation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated component testing with proper cleanup
  • Comprehensive edge case coverage
  • Type-safe test data structures
  • Proper mocking of external dependencies
  • Consistent test organization and naming
  • Clear test descriptions and assertions

gradio-app/gradio

js/chatbot/Chatbot.test.ts

            
import { test, describe, assert, afterEach, vi } from "vitest";
import { cleanup, render, fireEvent } from "@self/tootils";
import Chatbot from "./Index.svelte";
import type { LoadingStatus } from "@gradio/statustracker";
import type { FileData } from "@gradio/client";

const loading_status: LoadingStatus = {
	eta: 0,
	queue_position: 1,
	queue_size: 1,
	status: "complete",
	scroll_to_output: false,
	visible: true,
	fn_index: 0,
	show_progress: "full"
};

describe("Chatbot", () => {
	afterEach(() => cleanup());

	test("renders user and bot messages", async () => {
		const { getAllByTestId } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: [["user message one", "bot message one"]],
			latex_delimiters: [{ left: "$$", right: "$$", display: true }]
		});

		const bot = getAllByTestId("user")[0];
		const user = getAllByTestId("bot")[0];

		assert.exists(bot);
		assert.exists(user);
	});

	test("null messages are not visible", async () => {
		const { getByRole, container } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: [[null, null]],
			latex_delimiters: [{ left: "$$", right: "$$", display: true }]
		});

		const chatbot = getByRole("log");

		const userButton = container.querySelector(".user button");
		const botButton = container.querySelector(".bot button");

		assert.notExists(userButton);
		assert.notExists(botButton);

		assert.isFalse(chatbot.innerHTML.includes("button"));
	});

	test("empty string messages are visible", async () => {
		const { container } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: [["", ""]],
			latex_delimiters: [{ left: "$$", right: "$$", display: true }]
		});

		const userButton = container.querySelector(".user button");
		const botButton = container.querySelector(".bot button");

		assert.exists(userButton);
		assert.exists(botButton);
	});

	test("renders additional message as they are passed", async () => {
		const { component, getAllByTestId } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: [["user message one", "bot message one"]],
			latex_delimiters: [{ left: "$$", right: "$$", display: true }]
		});

		await component.$set({
			value: [
				["user message one", "bot message one"],
				["user message two", "bot message two"]
			]
		});

		const user_2 = getAllByTestId("user");
		const bot_2 = getAllByTestId("bot");

		assert.equal(user_2.length, 2);
		assert.equal(bot_2.length, 2);

		assert.exists(user_2[1]);
		assert.exists(bot_2[1]);
	});

	test.skip("renders image bot and user messages", async () => {
		const { component, getAllByTestId, debug } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: undefined,
			latex_delimiters: []
		});

		let value: [string | FileData | null, string | FileData | null][] = Array(
			2
		).fill([
			{
				file: {
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/cheetah1.jpg",
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/cheetah1.jpg",
					mime_type: "image/jpeg",
					alt_text: null
				}
			}
		]);

		await component.$set({
			value: value
		});

		const image = getAllByTestId("chatbot-image") as HTMLImageElement[];
		debug(image[0]);
		assert.isTrue(image[0].src.includes("cheetah1.jpg"));
		assert.isTrue(image[1].src.includes("cheetah1.jpg"));
	});

	test.skip("renders video bot and user messages", async () => {
		const { component, getAllByTestId } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			latex_delimiters: [],
			theme_mode: "dark"
		});
		let value: Array<[string | FileData | null, string | FileData | null]> =
			Array(2).fill([
				{
					file: {
						path: "https://gradio-builds.s3.amazonaws.com/demo-files/video_sample.mp4",
						url: "https://gradio-builds.s3.amazonaws.com/demo-files/video_sample.mp4",
						mime_type: "video/mp4",
						alt_text: null
					}
				}
			]);
		await component.$set({
			value: value
		});

		const video = getAllByTestId("chatbot-video") as HTMLVideoElement[];
		assert.isTrue(video[0].src.includes("video_sample.mp4"));
		assert.isTrue(video[1].src.includes("video_sample.mp4"));
	});

	test.skip("renders audio bot and user messages", async () => {
		const { component, getAllByTestId } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			latex_delimiters: [],
			theme_mode: "dark"
		});

		let value = Array(2).fill([
			{
				file: {
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
					mime_type: "audio/wav",
					alt_text: null
				}
			}
		]);

		await component.$set({
			value: value
		});

		const audio = getAllByTestId("chatbot-audio") as HTMLAudioElement[];
		assert.isTrue(audio[0].src.includes("audio_sample.wav"));
		assert.isTrue(audio[1].src.includes("audio_sample.wav"));
	});

	test("renders hyperlinks to file bot and user messages", async () => {
		const { component, getAllByTestId } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			latex_delimiters: []
		});

		let value = Array(2).fill([
			{
				file: {
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/titanic.csv",
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/titanic.csv",
					mime_type: "text/csv",
					alt_text: null
				}
			}
		]);

		await component.$set({
			value: value
		});

		const file_link = getAllByTestId("chatbot-file") as HTMLAnchorElement[];
		assert.isTrue(file_link[0].href.includes("titanic.csv"));
		assert.isTrue(file_link[0].href.includes("titanic.csv"));
	});

	test("renders copy all messages button and copies all messages to clipboard", async () => {
		// mock the clipboard API
		const clipboard_write_text_mock = vi.fn().mockResolvedValue(undefined);

		Object.defineProperty(navigator, "clipboard", {
			value: { writeText: clipboard_write_text_mock },
			configurable: true,
			writable: true
		});

		const { getByLabelText } = await render(Chatbot, {
			loading_status,
			label: "chatbot",
			value: [["user message one", "bot message one"]],
			show_copy_all_button: true
		});

		const copy_button = getByLabelText("Copy conversation");

		fireEvent.click(copy_button);

		expect(clipboard_write_text_mock).toHaveBeenCalledWith(
			expect.stringContaining("user: user message one")
		);
		expect(clipboard_write_text_mock).toHaveBeenCalledWith(
			expect.stringContaining("assistant: bot message one")
		);
	});
});