162
test/kijiji-core.test.ts
Normal file
162
test/kijiji-core.test.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import {
|
||||||
|
resolveLocationId,
|
||||||
|
resolveCategoryId,
|
||||||
|
buildSearchUrl,
|
||||||
|
HttpError,
|
||||||
|
NetworkError,
|
||||||
|
ParseError,
|
||||||
|
RateLimitError,
|
||||||
|
ValidationError
|
||||||
|
} from "../src/kijiji";
|
||||||
|
|
||||||
|
describe("Location and Category Resolution", () => {
|
||||||
|
describe("resolveLocationId", () => {
|
||||||
|
test("should return numeric IDs as-is", () => {
|
||||||
|
expect(resolveLocationId(1700272)).toBe(1700272);
|
||||||
|
expect(resolveLocationId(0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve string location names", () => {
|
||||||
|
expect(resolveLocationId("canada")).toBe(0);
|
||||||
|
expect(resolveLocationId("ontario")).toBe(9004);
|
||||||
|
expect(resolveLocationId("toronto")).toBe(1700273);
|
||||||
|
expect(resolveLocationId("gta")).toBe(1700272);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle case insensitive matching", () => {
|
||||||
|
expect(resolveLocationId("Canada")).toBe(0);
|
||||||
|
expect(resolveLocationId("ONTARIO")).toBe(9004);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should default to Canada for unknown locations", () => {
|
||||||
|
expect(resolveLocationId("unknown")).toBe(0);
|
||||||
|
expect(resolveLocationId("")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle undefined input", () => {
|
||||||
|
expect(resolveLocationId(undefined)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveCategoryId", () => {
|
||||||
|
test("should return numeric IDs as-is", () => {
|
||||||
|
expect(resolveCategoryId(132)).toBe(132);
|
||||||
|
expect(resolveCategoryId(0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve string category names", () => {
|
||||||
|
expect(resolveCategoryId("all")).toBe(0);
|
||||||
|
expect(resolveCategoryId("phones")).toBe(132);
|
||||||
|
expect(resolveCategoryId("electronics")).toBe(29659001);
|
||||||
|
expect(resolveCategoryId("buy-sell")).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle case insensitive matching", () => {
|
||||||
|
expect(resolveCategoryId("All")).toBe(0);
|
||||||
|
expect(resolveCategoryId("PHONES")).toBe(132);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should default to all categories for unknown categories", () => {
|
||||||
|
expect(resolveCategoryId("unknown")).toBe(0);
|
||||||
|
expect(resolveCategoryId("")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle undefined input", () => {
|
||||||
|
expect(resolveCategoryId(undefined)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("URL Construction", () => {
|
||||||
|
describe("buildSearchUrl", () => {
|
||||||
|
test("should build basic search URL", () => {
|
||||||
|
const url = buildSearchUrl("iphone", {
|
||||||
|
location: 1700272,
|
||||||
|
category: 132,
|
||||||
|
sortBy: 'relevancy',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(url).toContain("b-buy-sell/canada/iphone/k0c132l1700272");
|
||||||
|
expect(url).toContain("sort=relevancyDesc");
|
||||||
|
expect(url).toContain("order=DESC");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle pagination", () => {
|
||||||
|
const url = buildSearchUrl("iphone", {
|
||||||
|
location: 1700272,
|
||||||
|
category: 132,
|
||||||
|
page: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(url).toContain("&page=2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle different sort options", () => {
|
||||||
|
const dateUrl = buildSearchUrl("iphone", {
|
||||||
|
sortBy: 'date',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
});
|
||||||
|
expect(dateUrl).toContain("sort=DATE");
|
||||||
|
expect(dateUrl).toContain("order=ASC");
|
||||||
|
|
||||||
|
const priceUrl = buildSearchUrl("iphone", {
|
||||||
|
sortBy: 'price',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
expect(priceUrl).toContain("sort=PRICE");
|
||||||
|
expect(priceUrl).toContain("order=DESC");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle string location/category inputs", () => {
|
||||||
|
const url = buildSearchUrl("iphone", {
|
||||||
|
location: "toronto",
|
||||||
|
category: "phones",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(url).toContain("k0c132l1700273"); // phones + toronto
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Classes", () => {
|
||||||
|
test("HttpError should store status and URL", () => {
|
||||||
|
const error = new HttpError("Not found", 404, "https://example.com");
|
||||||
|
expect(error.message).toBe("Not found");
|
||||||
|
expect(error.status).toBe(404);
|
||||||
|
expect(error.url).toBe("https://example.com");
|
||||||
|
expect(error.name).toBe("HttpError");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("NetworkError should store URL and cause", () => {
|
||||||
|
const cause = new Error("Connection failed");
|
||||||
|
const error = new NetworkError("Network error", "https://example.com", cause);
|
||||||
|
expect(error.message).toBe("Network error");
|
||||||
|
expect(error.url).toBe("https://example.com");
|
||||||
|
expect(error.cause).toBe(cause);
|
||||||
|
expect(error.name).toBe("NetworkError");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ParseError should store data", () => {
|
||||||
|
const data = { invalid: "json" };
|
||||||
|
const error = new ParseError("Invalid JSON", data);
|
||||||
|
expect(error.message).toBe("Invalid JSON");
|
||||||
|
expect(error.data).toBe(data);
|
||||||
|
expect(error.name).toBe("ParseError");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("RateLimitError should store URL and reset time", () => {
|
||||||
|
const error = new RateLimitError("Rate limited", "https://example.com", 60);
|
||||||
|
expect(error.message).toBe("Rate limited");
|
||||||
|
expect(error.url).toBe("https://example.com");
|
||||||
|
expect(error.resetTime).toBe(60);
|
||||||
|
expect(error.name).toBe("RateLimitError");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ValidationError should work without field", () => {
|
||||||
|
const error = new ValidationError("Invalid value");
|
||||||
|
expect(error.message).toBe("Invalid value");
|
||||||
|
expect(error.name).toBe("ValidationError");
|
||||||
|
});
|
||||||
|
});
|
||||||
337
test/kijiji-integration.test.ts
Normal file
337
test/kijiji-integration.test.ts
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import { extractApolloState, parseSearch, parseDetailedListing } from "../src/kijiji";
|
||||||
|
|
||||||
|
// Mock fetch globally
|
||||||
|
const originalFetch = global.fetch;
|
||||||
|
|
||||||
|
describe("HTML Parsing Integration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock fetch for all tests
|
||||||
|
global.fetch = mock(() => {
|
||||||
|
throw new Error("fetch should be mocked in individual tests");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractApolloState", () => {
|
||||||
|
test("should extract Apollo state from valid HTML", () => {
|
||||||
|
const mockHtml = '<html><head><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"__APOLLO_STATE__":{"ROOT_QUERY":{"test":"value"}}}}}</script></head></html>';
|
||||||
|
|
||||||
|
const result = extractApolloState(mockHtml);
|
||||||
|
expect(result).toEqual({
|
||||||
|
ROOT_QUERY: { test: "value" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return null for HTML without Apollo state", () => {
|
||||||
|
const mockHtml = '<html><body>No data here</body></html>';
|
||||||
|
const result = extractApolloState(mockHtml);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return null for malformed JSON", () => {
|
||||||
|
const mockHtml = '<html><script id="__NEXT_DATA__" type="application/json">{"invalid": json}</script></html>';
|
||||||
|
|
||||||
|
const result = extractApolloState(mockHtml);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle missing __NEXT_DATA__ element", () => {
|
||||||
|
const mockHtml = '<html><body><div>Content</div></body></html>';
|
||||||
|
const result = extractApolloState(mockHtml);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseSearch", () => {
|
||||||
|
test("should parse search results from HTML", () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "/v-iphone/k0l0",
|
||||||
|
title: "iPhone 13 Pro",
|
||||||
|
},
|
||||||
|
"Listing:456": {
|
||||||
|
url: "/v-samsung/k0l0",
|
||||||
|
title: "Samsung Galaxy",
|
||||||
|
},
|
||||||
|
"ROOT_QUERY": { test: "value" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const results = parseSearch(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
name: "iPhone 13 Pro",
|
||||||
|
listingLink: "https://www.kijiji.ca/v-iphone/k0l0"
|
||||||
|
});
|
||||||
|
expect(results[1]).toEqual({
|
||||||
|
name: "Samsung Galaxy",
|
||||||
|
listingLink: "https://www.kijiji.ca/v-samsung/k0l0"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle absolute URLs", () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "https://www.kijiji.ca/v-iphone/k0l0",
|
||||||
|
title: "iPhone 13 Pro",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const results = parseSearch(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(results[0].listingLink).toBe("https://www.kijiji.ca/v-iphone/k0l0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should filter out invalid listings", () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "/v-iphone/k0l0",
|
||||||
|
title: "iPhone 13 Pro",
|
||||||
|
},
|
||||||
|
"Listing:456": {
|
||||||
|
url: "/v-samsung/k0l0",
|
||||||
|
// Missing title
|
||||||
|
},
|
||||||
|
"Other:789": {
|
||||||
|
url: "/v-other/k0l0",
|
||||||
|
title: "Other Item",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const results = parseSearch(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].name).toBe("iPhone 13 Pro");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return empty array for invalid HTML", () => {
|
||||||
|
const results = parseSearch("<html><body>Invalid</body></html>", "https://www.kijiji.ca");
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseDetailedListing", () => {
|
||||||
|
test("should parse detailed listing with all fields", async () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "/v-iphone-13-pro/k0l0",
|
||||||
|
title: "iPhone 13 Pro 256GB",
|
||||||
|
description: "Excellent condition iPhone 13 Pro",
|
||||||
|
price: {
|
||||||
|
amount: 80000,
|
||||||
|
currency: "CAD",
|
||||||
|
type: "FIXED"
|
||||||
|
},
|
||||||
|
type: "OFFER",
|
||||||
|
status: "ACTIVE",
|
||||||
|
activationDate: "2024-01-15T10:00:00.000Z",
|
||||||
|
endDate: "2025-01-15T10:00:00.000Z",
|
||||||
|
metrics: { views: 150 },
|
||||||
|
location: {
|
||||||
|
address: "Toronto, ON",
|
||||||
|
id: 1700273,
|
||||||
|
name: "Toronto",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 43.6532,
|
||||||
|
longitude: -79.3832
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageUrls: [
|
||||||
|
"https://media.kijiji.ca/api/v1/image1.jpg",
|
||||||
|
"https://media.kijiji.ca/api/v1/image2.jpg"
|
||||||
|
],
|
||||||
|
imageCount: 2,
|
||||||
|
categoryId: 132,
|
||||||
|
adSource: "ORGANIC",
|
||||||
|
flags: {
|
||||||
|
topAd: false,
|
||||||
|
priceDrop: true
|
||||||
|
},
|
||||||
|
posterInfo: {
|
||||||
|
posterId: "user123",
|
||||||
|
rating: 4.8
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{ canonicalName: "forsaleby", canonicalValues: ["ownr"] },
|
||||||
|
{ canonicalName: "phonecarrier", canonicalValues: ["unlocked"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await parseDetailedListing(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "https://www.kijiji.ca/v-iphone-13-pro/k0l0",
|
||||||
|
title: "iPhone 13 Pro 256GB",
|
||||||
|
description: "Excellent condition iPhone 13 Pro",
|
||||||
|
listingPrice: {
|
||||||
|
amountFormatted: "$800.00",
|
||||||
|
cents: 80000,
|
||||||
|
currency: "CAD"
|
||||||
|
},
|
||||||
|
listingType: "OFFER",
|
||||||
|
listingStatus: "ACTIVE",
|
||||||
|
creationDate: "2024-01-15T10:00:00.000Z",
|
||||||
|
endDate: "2025-01-15T10:00:00.000Z",
|
||||||
|
numberOfViews: 150,
|
||||||
|
address: "Toronto, ON",
|
||||||
|
images: [
|
||||||
|
"https://media.kijiji.ca/api/v1/image1.jpg",
|
||||||
|
"https://media.kijiji.ca/api/v1/image2.jpg"
|
||||||
|
],
|
||||||
|
categoryId: 132,
|
||||||
|
adSource: "ORGANIC",
|
||||||
|
flags: {
|
||||||
|
topAd: false,
|
||||||
|
priceDrop: true
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
forsaleby: ["ownr"],
|
||||||
|
phonecarrier: ["unlocked"]
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
id: 1700273,
|
||||||
|
name: "Toronto",
|
||||||
|
coordinates: {
|
||||||
|
latitude: 43.6532,
|
||||||
|
longitude: -79.3832
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sellerInfo: {
|
||||||
|
posterId: "user123",
|
||||||
|
rating: 4.8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return null for contact-based pricing", async () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "/v-iphone/k0l0",
|
||||||
|
title: "iPhone for Sale",
|
||||||
|
price: {
|
||||||
|
type: "CONTACT",
|
||||||
|
amount: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await parseDetailedListing(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle missing optional fields", async () => {
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<script id="__NEXT_DATA__" type="application/json">
|
||||||
|
${JSON.stringify({
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
__APOLLO_STATE__: {
|
||||||
|
"Listing:123": {
|
||||||
|
url: "/v-iphone/k0l0",
|
||||||
|
title: "iPhone 13",
|
||||||
|
price: { amount: 50000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await parseDetailedListing(mockHtml, "https://www.kijiji.ca");
|
||||||
|
expect(result).toEqual({
|
||||||
|
url: "https://www.kijiji.ca/v-iphone/k0l0",
|
||||||
|
title: "iPhone 13",
|
||||||
|
description: undefined,
|
||||||
|
listingPrice: {
|
||||||
|
amountFormatted: "$500.00",
|
||||||
|
cents: 50000,
|
||||||
|
currency: undefined
|
||||||
|
},
|
||||||
|
listingType: undefined,
|
||||||
|
listingStatus: undefined,
|
||||||
|
creationDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
numberOfViews: undefined,
|
||||||
|
address: null,
|
||||||
|
images: [],
|
||||||
|
categoryId: 0,
|
||||||
|
adSource: "UNKNOWN",
|
||||||
|
flags: {
|
||||||
|
topAd: false,
|
||||||
|
priceDrop: false
|
||||||
|
},
|
||||||
|
attributes: {},
|
||||||
|
location: {
|
||||||
|
id: 0,
|
||||||
|
name: "Unknown",
|
||||||
|
coordinates: undefined
|
||||||
|
},
|
||||||
|
sellerInfo: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
54
test/kijiji-utils.test.ts
Normal file
54
test/kijiji-utils.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { slugify, formatCentsToCurrency } from "../src/kijiji";
|
||||||
|
|
||||||
|
describe("Utility Functions", () => {
|
||||||
|
describe("slugify", () => {
|
||||||
|
test("should convert basic strings to slugs", () => {
|
||||||
|
expect(slugify("Hello World")).toBe("hello-world");
|
||||||
|
expect(slugify("iPhone 13 Pro")).toBe("iphone-13-pro");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle special characters", () => {
|
||||||
|
expect(slugify("Café & Restaurant")).toBe("cafe-restaurant");
|
||||||
|
expect(slugify("100% New")).toBe("100-new");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle empty and edge cases", () => {
|
||||||
|
expect(slugify("")).toBe("");
|
||||||
|
expect(slugify(" ")).toBe("-");
|
||||||
|
expect(slugify("---")).toBe("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should preserve numbers and valid characters", () => {
|
||||||
|
expect(slugify("iPhone 13")).toBe("iphone-13");
|
||||||
|
expect(slugify("item123")).toBe("item123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatCentsToCurrency", () => {
|
||||||
|
test("should format valid cent values", () => {
|
||||||
|
expect(formatCentsToCurrency(100)).toBe("$1.00");
|
||||||
|
expect(formatCentsToCurrency(1999)).toBe("$19.99");
|
||||||
|
expect(formatCentsToCurrency(0)).toBe("$0.00");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle string inputs", () => {
|
||||||
|
expect(formatCentsToCurrency("100")).toBe("$1.00");
|
||||||
|
expect(formatCentsToCurrency("1999")).toBe("$19.99");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle null/undefined inputs", () => {
|
||||||
|
expect(formatCentsToCurrency(null)).toBe("");
|
||||||
|
expect(formatCentsToCurrency(undefined)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle invalid inputs", () => {
|
||||||
|
expect(formatCentsToCurrency("invalid")).toBe("");
|
||||||
|
expect(formatCentsToCurrency(Number.NaN)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use en-US locale formatting", () => {
|
||||||
|
expect(formatCentsToCurrency(123456)).toBe("$1,234.56");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user