Back to Repositories

Testing API Information Handling Implementation in Gradio

This test suite validates the API information handling functionality in the Gradio client application. It covers message handling, endpoint processing, and parameter mapping with comprehensive unit tests using Jest and TypeScript.

Test Coverage Overview

The test suite provides extensive coverage of API message handling and endpoint processing.

Key areas tested include:
  • Message type handling and status updates
  • Endpoint URL processing and validation
  • Parameter mapping and validation
  • Error handling and edge cases
  • Space metadata processing

Implementation Analysis

The implementation uses Jest’s describe/it pattern for organized test grouping. Tests utilize TypeScript for type safety and mock servers for API testing.

Key patterns include:
  • Isolated test cases with clear assertions
  • Comprehensive error scenario coverage
  • Mock server integration for endpoint testing
  • Type-safe parameter validation

Technical Details

Testing tools and setup:
  • Jest test framework
  • TypeScript for type checking
  • Vitest for test running
  • Mock server for API simulation
  • Custom test data fixtures
  • Beforeall/afterall hooks for test lifecycle

Best Practices Demonstrated

The test suite demonstrates strong testing practices with isolated, focused test cases and comprehensive coverage.

Notable practices include:
  • Clear test case organization
  • Thorough error handling validation
  • Mock server usage for API testing
  • Type-safe implementations
  • Comprehensive edge case coverage

gradio-app/gradio

client/js/src/test/api_info.test.ts

            
import {
	INVALID_URL_MSG,
	QUEUE_FULL_MSG,
	SPACE_METADATA_ERROR_MSG
} from "../constants";
import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest";
import {
	handle_message,
	get_description,
	get_type,
	process_endpoint,
	join_urls,
	map_data_to_params
} from "../helpers/api_info";
import { initialise_server } from "./server";
import { transformed_api_info } from "./test_data";

const server = initialise_server();

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("handle_message", () => {
	it("should return type 'data' when msg is 'send_data'", () => {
		const data = { msg: "send_data" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({ type: "data" });
	});

	it("should return type 'hash' when msg is 'send_hash'", () => {
		const data = { msg: "send_hash" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({ type: "hash" });
	});

	it("should return type 'update' with queue full message when msg is 'queue_full'", () => {
		const data = { msg: "queue_full", code: 500, success: false };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "update",
			status: {
				queue: true,
				message: QUEUE_FULL_MSG,
				stage: "error",
				code: 500,
				success: false
			}
		});
	});

	it("should return type 'heartbeat' when msg is 'heartbeat'", () => {
		const data = { msg: "heartbeat" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({ type: "heartbeat" });
	});

	it("should return type 'unexpected_error' with error message when msg is 'unexpected_error'", () => {
		const data = { msg: "unexpected_error", message: "Something went wrong" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "unexpected_error",
			status: {
				queue: true,
				message: "Something went wrong",
				stage: "error",
				success: false
			}
		});
	});

	it("should return type 'update' with estimation status when msg is 'estimation'", () => {
		const data = {
			msg: "estimation",
			code: 200,
			queue_size: 10,
			rank: 5,
			rank_eta: 60,
			success: true
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "update",
			status: {
				queue: true,
				stage: "pending",
				code: 200,
				size: 10,
				position: 5,
				eta: 60,
				success: true
			}
		});
	});

	it("should return type 'update' with progress status when msg is 'progress'", () => {
		const data = {
			msg: "progress",
			code: 200,
			progress_data: { current: 50, total: 100 },
			success: true
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "update",
			status: {
				queue: true,
				stage: "pending",
				code: 200,
				progress_data: { current: 50, total: 100 },
				success: true
			}
		});
	});

	it("should return type 'log' with the provided data when msg is 'log'", () => {
		const data = { msg: "log", log_data: "Some log message" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "log",
			data: { msg: "log", log_data: "Some log message" }
		});
	});

	it("should return type 'generating' with generating status when msg is 'process_generating' and success is true", () => {
		const data = {
			msg: "process_generating",
			success: true,
			code: 200,
			progress_data: { current: 50, total: 100 },
			average_duration: 120,
			output: { result: "Some result" }
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "generating",
			status: {
				queue: true,
				message: null,
				stage: "generating",
				code: 200,
				progress_data: { current: 50, total: 100 },
				eta: 120
			},
			data: { result: "Some result" }
		});
	});

	it("should return type 'update' with error status when msg is 'process_generating' and success is false", () => {
		const data = {
			msg: "process_generating",
			success: false,
			code: 500,
			progress_data: { current: 50, total: 100 },
			average_duration: 120,
			output: { error: "Error" }
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);

		expect(result).toEqual({
			type: "generating",
			data: null,
			status: {
				eta: 120,
				queue: true,
				message: "Error",
				stage: "error",
				code: 500,
				progress_data: { current: 50, total: 100 }
			}
		});
	});

	it("should return type 'complete' with success status when msg is 'process_completed' and success is true", () => {
		const data = {
			msg: "process_completed",
			success: true,
			code: 200,
			progress_data: { current: 100, total: 100 },
			output: { result: "Some result" }
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "complete",
			status: {
				queue: true,
				message: undefined,
				stage: "complete",
				code: 200,
				progress_data: { current: 100, total: 100 }
			},
			data: { result: "Some result" }
		});
	});

	it("should return type 'update' with error status when msg is 'process_completed' and success is false", () => {
		const data = {
			msg: "process_completed",
			success: false,
			code: 500,
			progress_data: { current: 100, total: 100 },
			output: { error: "Some error message" }
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "update",
			status: {
				queue: true,
				message: "Some error message",
				stage: "error",
				code: 500,
				success: false
			}
		});
	});

	it("should return type 'update' with pending status when msg is 'process_starts'", () => {
		const data = {
			msg: "process_starts",
			code: 200,
			rank: 5,
			success: true,
			eta: 60
		};
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "update",
			original_msg: "process_starts",
			status: {
				queue: true,
				stage: "pending",
				code: 200,
				size: 5,
				position: 0,
				success: true,
				eta: 60
			}
		});
	});

	it("should return type 'none' with error status when msg is unknown", () => {
		const data = { msg: "unknown" };
		const last_status = "pending";
		const result = handle_message(data, last_status);
		expect(result).toEqual({
			type: "none",
			status: { stage: "error", queue: true }
		});
	});
});

describe("get_description", () => {
	it("should return 'array of [file, label] tuples' when serializer is 'GallerySerializable'", () => {
		const type = { type: "string", description: "param description" };
		const serializer = "GallerySerializable";
		const result = get_description(type, serializer);
		expect(result).toEqual("array of [file, label] tuples");
	});

	it("should return 'array of strings' when serializer is 'ListStringSerializable'", () => {
		const type = { type: "string", description: "param description" };
		const serializer = "ListStringSerializable";
		const result = get_description(type, serializer);
		expect(result).toEqual("array of strings");
	});

	it("should return 'array of files or single file' when serializer is 'FileSerializable'", () => {
		const type = { type: "string", description: "param description" };
		const serializer = "FileSerializable";
		const result = get_description(type, serializer);
		expect(result).toEqual("array of files or single file");
	});

	it("should return the type's description when serializer is not 'GallerySerializable', 'ListStringSerializable', or 'FileSerializable'", () => {
		const type = { type: "string", description: "param description" };
		const serializer = "SomeOtherSerializer";
		const result = get_description(type, serializer);
		expect(result).toEqual(type.description);
	});
});

describe("get_type", () => {
	it("should return 'string' when type is 'string'", () => {
		const type = { type: "string", description: "param description" };
		const component = "Component";
		const serializer = "Serializer";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("string");
	});

	it("should return 'boolean' when type is 'boolean'", () => {
		const type = { type: "boolean", description: "param description" };
		const component = "Component";
		const serializer = "Serializer";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("boolean");
	});

	it("should return 'number' when type is 'number'", () => {
		const type = { type: "number", description: "param description" };
		const component = "Component";
		const serializer = "Serializer";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("number");
	});

	it("should return 'any' when serializer is 'JSONSerializable'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "JSONSerializable";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("any");
	});

	it("should return 'any' when serializer is 'StringSerializable'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "StringSerializable";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("any");
	});

	it("should return 'string[]' when serializer is 'ListStringSerializable'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "ListStringSerializable";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("string[]");
	});

	it("should return 'Blob | File | Buffer' when component is 'Image' and signature_type is 'parameter'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Image";
		const serializer = "Serializer";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("Blob | File | Buffer");
	});

	it("should return 'string' when component is 'Image' and signature_type is 'return'", () => {
		const type = { type: "string", description: "param description" };
		const component = "Image";
		const serializer = "Serializer";
		const signature_type = "return";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("string");
	});

	it("should return '(Blob | File | Buffer)[]' when serializer is 'FileSerializable' and type is an array and signature_type is 'parameter'", () => {
		const type = { type: "array", description: "param description" };
		const component = "Component";
		const serializer = "FileSerializable";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("(Blob | File | Buffer)[]");
	});

	it("should return 'Blob | File | Buffer' when serializer is 'FileSerializable' and type is not an array and signature_type is 'return'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "FileSerializable";
		const signature_type = "return";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual(
			"{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}"
		);
	});

	it("should return a FileData object when serializer is 'FileSerializable' and type is not an array and signature_type is 'return'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "FileSerializable";
		const signature_type = "return";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual(
			"{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}"
		);
	});

	it("should return '[(Blob | File | Buffer), (string | null)][]' when serializer is 'GallerySerializable' and signature_type is 'parameter'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "GallerySerializable";
		const signature_type = "parameter";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual("[(Blob | File | Buffer), (string | null)][]");
	});

	it("should return a FileData object when serializer is 'GallerySerializable' and signature_type is 'return'", () => {
		const type = { type: "any", description: "param description" };
		const component = "Component";
		const serializer = "GallerySerializable";
		const signature_type = "return";
		const result = get_type(type, component, serializer, signature_type);
		expect(result).toEqual(
			"[{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}, (string | null))][]"
		);
	});
});

describe("process_endpoint", () => {
	it("should return space_id, host, ws_protocol, and http_protocol when app_reference is a valid space name", async () => {
		const app_reference = "hmb/hello_world";
		const host = "hmb-hello-world.hf.space";

		const hf_token = "hf_token";
		const expected = {
			space_id: app_reference,
			host,
			ws_protocol: "wss",
			http_protocol: "https:"
		};

		const result = await process_endpoint(app_reference, hf_token);
		expect(result).toEqual(expected);
	});

	it("should throw an error when fetching space metadata fails", async () => {
		const app_reference = "hmb/bye_world";
		const hf_token = "hf_token";

		try {
			await process_endpoint(app_reference, hf_token);
		} catch (error) {
			expect(error.message).toEqual(SPACE_METADATA_ERROR_MSG);
		}
	});

	it("should return the correct data when app_reference is a valid space domain", async () => {
		const app_reference = "hmb/hello_world";
		const host = "hmb-hello-world.hf.space";

		const expected = {
			space_id: app_reference,
			host,
			ws_protocol: "wss",
			http_protocol: "https:"
		};

		const result = await process_endpoint("hmb/hello_world");
		expect(result).toEqual(expected);
	});

	it("processes local server URLs correctly", async () => {
		const local_url = "http://localhost:7860/gradio";
		const response_local_url = await process_endpoint(local_url);
		expect(response_local_url.space_id).toBe(false);
		expect(response_local_url.host).toBe("localhost:7860/gradio");

		const local_url_2 = "http://localhost:7860/gradio/";
		const response_local_url_2 = await process_endpoint(local_url_2);
		expect(response_local_url_2.space_id).toBe(false);
		expect(response_local_url_2.host).toBe("localhost:7860/gradio");
	});

	it("handles hugging face space references", async () => {
		const space_id = "hmb/hello_world";

		const response = await process_endpoint(space_id);
		expect(response.space_id).toBe(space_id);
		expect(response.host).toContain("hf.space");
	});

	it("handles hugging face domain URLs", async () => {
		const app_reference = "https://hmb-hello-world.hf.space/";
		const response = await process_endpoint(app_reference);
		expect(response.space_id).toBe("hmb-hello-world");
		expect(response.host).toBe("hmb-hello-world.hf.space");
	});

	it("handles huggingface subpath urls", async () => {
		const app_reference =
			"https://pngwn-pr-demos-test.hf.space/demo/audio_debugger/";
		const response = await process_endpoint(app_reference);
		expect(response.space_id).toBe("pngwn-pr-demos-test");
		expect(response.host).toBe(
			"pngwn-pr-demos-test.hf.space/demo/audio_debugger"
		);

		expect(response.http_protocol).toBe("https:");
	});
});

describe("join_urls", () => {
	it("joins URLs correctly", () => {
		expect(join_urls("http://localhost:7860", "/gradio")).toBe(
			"http://localhost:7860/gradio"
		);
		expect(join_urls("http://localhost:7860/", "/gradio")).toBe(
			"http://localhost:7860/gradio"
		);
		expect(join_urls("http://localhost:7860", "app/", "/gradio")).toBe(
			"http://localhost:7860/app/gradio"
		);
		expect(join_urls("http://localhost:7860/", "/app/", "/gradio/")).toBe(
			"http://localhost:7860/app/gradio/"
		);

		expect(join_urls("http://127.0.0.1:8000/app", "/config")).toBe(
			"http://127.0.0.1:8000/app/config"
		);

		expect(join_urls("http://127.0.0.1:8000/app/gradio", "/config")).toBe(
			"http://127.0.0.1:8000/app/gradio/config"
		);
	});
	it("throws an error when the URLs are not valid", () => {
		expect(() => join_urls("localhost:7860", "/gradio")).toThrowError(
			INVALID_URL_MSG
		);

		expect(() => join_urls("localhost:7860", "/gradio", "app")).toThrowError(
			INVALID_URL_MSG
		);
	});
});

describe("map_data_params", () => {
	let test_data = transformed_api_info;

	test_data.named_endpoints["/predict"].parameters = [
		{
			parameter_name: "param1",
			parameter_has_default: false,
			label: "",
			component: "",
			serializer: "",
			python_type: {
				type: "",
				description: ""
			},
			type: {
				type: "",
				description: ""
			}
		},
		{
			parameter_name: "param2",
			parameter_has_default: false,
			label: "",
			type: {
				type: "",
				description: ""
			},
			component: "",
			serializer: "",
			python_type: {
				type: "",
				description: ""
			}
		},
		{
			parameter_name: "param3",
			parameter_has_default: true,
			parameter_default: 3,
			label: "",
			type: {
				type: "",
				description: ""
			},
			component: "",
			serializer: "",
			python_type: {
				type: "",
				description: ""
			}
		}
	];

	let endpoint_info = test_data.named_endpoints["/predict"];

	it("should return an array of data when data is an array", () => {
		const data = [1, 2];

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual(data);
	});

	it("should return an empty array when data is an empty array", () => {
		const data = [];

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual(data);
	});

	it("should return an empty array when data is not defined", () => {
		const data = undefined;

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual([]);
	});

	it("should return the data when too many arguments are provided for the endpoint", () => {
		const data = [1, 2, 3, 4];

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual(data);
	});

	it("should return an array of resolved data when data is an object", () => {
		const data = {
			param1: 1,
			param2: 2,
			param3: 3
		};

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual([1, 2, 3]);
	});

	it("should use the default value when a keyword argument is not provided and has a default value", () => {
		const data = {
			param1: 1,
			param2: 2
		};

		const result = map_data_to_params(data, endpoint_info);
		expect(result).toEqual([1, 2, 3]);
	});

	it("should throw an error when an invalid keyword argument is provided", () => {
		const data = {
			param1: 1,
			param2: 2,
			param3: 3,
			param4: 4
		};

		expect(() => map_data_to_params(data, endpoint_info)).toThrowError(
			"Parameter `param4` is not a valid keyword argument. Please refer to the API for usage."
		);
	});

	it("should throw an error when no value is provided for a required parameter", () => {
		const data = {};

		expect(() => map_data_to_params(data, endpoint_info)).toThrowError(
			"No value provided for required parameter: param1"
		);
	});
});