feature/fetch-fonts #14
194
src/shared/lib/utils/buildQueryString/buildQueryString.test.ts
Normal file
194
src/shared/lib/utils/buildQueryString/buildQueryString.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Tests for buildQueryString utility
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
} from 'vitest';
|
||||
import { buildQueryString } from './buildQueryString';
|
||||
|
||||
describe('buildQueryString', () => {
|
||||
describe('basic parameter building', () => {
|
||||
test('should build query string with string parameter', () => {
|
||||
const result = buildQueryString({ category: 'serif' });
|
||||
expect(result).toBe('?category=serif');
|
||||
});
|
||||
|
||||
test('should build query string with number parameter', () => {
|
||||
const result = buildQueryString({ limit: 50 });
|
||||
expect(result).toBe('?limit=50');
|
||||
});
|
||||
|
||||
test('should build query string with boolean parameter', () => {
|
||||
const result = buildQueryString({ active: true });
|
||||
expect(result).toBe('?active=true');
|
||||
});
|
||||
|
||||
test('should build query string with multiple parameters', () => {
|
||||
const result = buildQueryString({
|
||||
category: 'serif',
|
||||
limit: 50,
|
||||
page: 1,
|
||||
});
|
||||
expect(result).toBe('?category=serif&limit=50&page=1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array handling', () => {
|
||||
test('should handle array of strings', () => {
|
||||
const result = buildQueryString({
|
||||
subsets: ['latin', 'latin-ext', 'cyrillic'],
|
||||
});
|
||||
expect(result).toBe('?subsets=latin&subsets=latin-ext&subsets=cyrillic');
|
||||
});
|
||||
|
||||
test('should handle array of numbers', () => {
|
||||
const result = buildQueryString({ ids: [1, 2, 3] });
|
||||
expect(result).toBe('?ids=1&ids=2&ids=3');
|
||||
});
|
||||
|
||||
test('should handle mixed arrays and primitives', () => {
|
||||
const result = buildQueryString({
|
||||
category: 'serif',
|
||||
subsets: ['latin', 'latin-ext'],
|
||||
limit: 50,
|
||||
});
|
||||
expect(result).toBe('?category=serif&subsets=latin&subsets=latin-ext&limit=50');
|
||||
});
|
||||
|
||||
test('should filter out null/undefined values in arrays', () => {
|
||||
const result = buildQueryString({
|
||||
// @ts-expect-error - Testing runtime behavior with invalid types
|
||||
ids: [1, null, 3, undefined],
|
||||
});
|
||||
expect(result).toBe('?ids=1&ids=3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('optional values', () => {
|
||||
test('should exclude undefined values', () => {
|
||||
const result = buildQueryString({
|
||||
category: 'serif',
|
||||
search: undefined,
|
||||
});
|
||||
expect(result).toBe('?category=serif');
|
||||
});
|
||||
|
||||
test('should exclude null values', () => {
|
||||
const result = buildQueryString({
|
||||
category: 'serif',
|
||||
search: null,
|
||||
});
|
||||
expect(result).toBe('?category=serif');
|
||||
});
|
||||
|
||||
test('should handle all undefined/null values', () => {
|
||||
const result = buildQueryString({
|
||||
category: undefined,
|
||||
search: null,
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL encoding', () => {
|
||||
test('should encode spaces', () => {
|
||||
const result = buildQueryString({ search: 'hello world' });
|
||||
expect(result).toBe('?search=hello+world');
|
||||
});
|
||||
|
||||
test('should encode special characters', () => {
|
||||
const result = buildQueryString({ query: 'a&b=c+d' });
|
||||
expect(result).toBe('?query=a%26b%3Dc%2Bd');
|
||||
});
|
||||
|
||||
test('should encode Unicode characters', () => {
|
||||
const result = buildQueryString({ text: 'café' });
|
||||
expect(result).toBe('?text=caf%C3%A9');
|
||||
});
|
||||
|
||||
test('should encode reserved URL characters', () => {
|
||||
const result = buildQueryString({ url: 'https://example.com' });
|
||||
expect(result).toBe('?url=https%3A%2F%2Fexample.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('should return empty string for empty object', () => {
|
||||
const result = buildQueryString({});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should return empty string when all values are excluded', () => {
|
||||
const result = buildQueryString({
|
||||
a: undefined,
|
||||
b: null,
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
const result = buildQueryString({ tags: [] });
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should handle zero values', () => {
|
||||
const result = buildQueryString({ page: 0, count: 0 });
|
||||
expect(result).toBe('?page=0&count=0');
|
||||
});
|
||||
|
||||
test('should handle false boolean', () => {
|
||||
const result = buildQueryString({ active: false });
|
||||
expect(result).toBe('?active=false');
|
||||
});
|
||||
|
||||
test('should handle empty string', () => {
|
||||
const result = buildQueryString({ search: '' });
|
||||
expect(result).toBe('?search=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parameter order', () => {
|
||||
test('should maintain parameter order from input object', () => {
|
||||
const result = buildQueryString({
|
||||
a: '1',
|
||||
b: '2',
|
||||
c: '3',
|
||||
});
|
||||
expect(result).toBe('?a=1&b=2&c=3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world examples', () => {
|
||||
test('should handle Google Fonts API parameters', () => {
|
||||
const result = buildQueryString({
|
||||
category: 'sans-serif',
|
||||
sort: 'popularity',
|
||||
subset: 'latin',
|
||||
});
|
||||
expect(result).toBe('?category=sans-serif&sort=popularity&subset=latin');
|
||||
});
|
||||
|
||||
test('should handle Fontshare API parameters', () => {
|
||||
const result = buildQueryString({
|
||||
categories: ['Sans', 'Serif'],
|
||||
page: 1,
|
||||
limit: 50,
|
||||
search: 'satoshi',
|
||||
});
|
||||
expect(result).toBe('?categories=Sans&categories=Serif&page=1&limit=50&search=satoshi');
|
||||
});
|
||||
|
||||
test('should handle pagination parameters', () => {
|
||||
const result = buildQueryString({
|
||||
page: 2,
|
||||
per_page: 20,
|
||||
sort: 'name',
|
||||
order: 'desc',
|
||||
});
|
||||
expect(result).toBe('?page=2&per_page=20&sort=name&order=desc');
|
||||
});
|
||||
});
|
||||
});
|
||||
79
src/shared/lib/utils/buildQueryString/buildQueryString.ts
Normal file
79
src/shared/lib/utils/buildQueryString/buildQueryString.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Build query string from URL parameters
|
||||
*
|
||||
* Generic, type-safe function to build properly encoded query strings
|
||||
* from URL parameters. Supports primitives, arrays, and optional values.
|
||||
*
|
||||
* @param params - Object containing query parameters
|
||||
* @returns Encoded query string (empty string if no parameters)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* buildQueryString({ category: 'serif', subsets: ['latin', 'latin-ext'] })
|
||||
* // Returns: "category=serif&subsets=latin&subsets=latin-ext"
|
||||
*
|
||||
* buildQueryString({ limit: 50, page: 1 })
|
||||
* // Returns: "limit=50&page=1"
|
||||
*
|
||||
* buildQueryString({})
|
||||
* // Returns: ""
|
||||
*
|
||||
* buildQueryString({ search: 'hello world', active: true })
|
||||
* // Returns: "search=hello%20world&active=true"
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Query parameter value type
|
||||
* Supports primitives, arrays, and excludes null/undefined
|
||||
*/
|
||||
export type QueryParamValue = string | number | boolean | string[] | number[];
|
||||
|
||||
/**
|
||||
* Query parameters object
|
||||
*/
|
||||
export type QueryParams = Record<string, QueryParamValue | undefined | null>;
|
||||
|
||||
/**
|
||||
* Build query string from URL parameters
|
||||
*
|
||||
* Handles:
|
||||
* - Primitive values (string, number, boolean)
|
||||
* - Arrays (multiple values with same key)
|
||||
* - Optional values (excludes undefined/null)
|
||||
* - Proper URL encoding
|
||||
*
|
||||
* Edge cases:
|
||||
* - Empty object → empty string
|
||||
* - No parameters → empty string
|
||||
* - Nested objects → flattens to string representation
|
||||
* - Special characters → proper encoding
|
||||
*
|
||||
* @param params - Object containing query parameters
|
||||
* @returns Encoded query string (with "?" prefix if non-empty)
|
||||
*/
|
||||
export function buildQueryString(params: QueryParams): string {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
// Skip undefined/null values
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle arrays (multiple values with same key)
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (item !== undefined && item !== null) {
|
||||
searchParams.append(key, String(item));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle primitives
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
}
|
||||
Reference in New Issue
Block a user