diff --git a/test/kijiji-core.test.ts b/test/kijiji-core.test.ts new file mode 100644 index 0000000..62a8f5b --- /dev/null +++ b/test/kijiji-core.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/test/kijiji-integration.test.ts b/test/kijiji-integration.test.ts new file mode 100644 index 0000000..32a2704 --- /dev/null +++ b/test/kijiji-integration.test.ts @@ -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 = '
'; + + const result = extractApolloState(mockHtml); + expect(result).toEqual({ + ROOT_QUERY: { test: "value" } + }); + }); + + test("should return null for HTML without Apollo state", () => { + const mockHtml = 'No data here'; + const result = extractApolloState(mockHtml); + expect(result).toBeNull(); + }); + + test("should return null for malformed JSON", () => { + const mockHtml = ''; + + const result = extractApolloState(mockHtml); + expect(result).toBeNull(); + }); + + test("should handle missing __NEXT_DATA__ element", () => { + const mockHtml = '