Spaces:
Paused
Paused
Download client/modules/stringFormatters.test.ts from Nymbo/MiniSearch: direct link, hf CLI and curl.
- Browser
- Download file 7.95 kB
-
https://huggingface.co/spaces/Nymbo/MiniSearch/resolve/main/client/modules/stringFormatters.test.ts
- Command line
-
hf download hf://spaces/Nymbo/MiniSearch/client/modules/stringFormatters.test.ts
-
curl -L -o stringFormatters.test.ts https://huggingface.co/spaces/Nymbo/MiniSearch/resolve/main/client/modules/stringFormatters.test.ts
7.95 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { | |
| formatRelativeTime, | |
| getHostname, | |
| getSemanticVersion, | |
| groupSearchResultsByDate, | |
| searchWithFuzzy, | |
| } from "./stringFormatters"; | |
| describe("stringFormatters", () => { | |
| describe("getHostname", () => { | |
| it.each([ | |
| ["https://example.com/page", "example.com"], | |
| ["https://www.example.com/page", "example.com"], | |
| // A subdomain survives. | |
| ["https://docs.example.com/page", "docs.example.com"], | |
| // Anchored, so `www.` deeper in the host stays. | |
| ["https://my.www.example.com/page", "my.www.example.com"], | |
| // Anchored, so an overlapping `www.` at offset 1 is not a prefix. | |
| ["https://wwww.example.com/page", "wwww.example.com"], | |
| // The strip runs once, not per occurrence. | |
| ["https://www.www.example.com/page", "www.example.com"], | |
| // The hostname, so the port stays out of it. | |
| ["https://localhost:3000", "localhost"], | |
| // Not a URL at all: hand back what came in, for display. | |
| ["not-a-url", "not-a-url"], | |
| ])("turns %s into %s", (url, expected) => { | |
| expect(getHostname(url)).toBe(expected); | |
| }); | |
| }); | |
| describe("getSemanticVersion", () => { | |
| it.each([ | |
| [1700000000000, "2023.11.14"], | |
| // Built in UTC, because the formatter reads UTC fields: a local-time Date | |
| // would format as the day before for anyone east of UTC. | |
| [new Date(Date.UTC(2024, 0, 15)), "2024.1.15"], | |
| // Read as UTC, so the fixture pins the same instant on every runner. | |
| ["2024-06-01", "2024.6.1"], | |
| ])("dates %s as %s", (date, expected) => { | |
| expect(getSemanticVersion(date)).toBe(expected); | |
| }); | |
| it("does not follow the runner's timezone", () => { | |
| const originalTimeZone = process.env.TZ; | |
| try { | |
| // A UTC-midnight instant reads as the previous day only in a zone | |
| // behind UTC; a zone ahead of it needs a late-in-the-day instant to | |
| // read as the next day. So each direction pins its own. The | |
| // getDate() checks make a silent no-op of the zone change fail | |
| // loudly instead of passing vacuously. | |
| process.env.TZ = "Pacific/Pago_Pago"; // UTC-11 | |
| const westMidnight = new Date(Date.UTC(2024, 0, 15)); | |
| expect(westMidnight.getDate()).toBe(14); | |
| expect(getSemanticVersion(westMidnight)).toBe("2024.1.15"); | |
| process.env.TZ = "Pacific/Kiritimati"; // UTC+14 | |
| const eastLate = new Date(Date.UTC(2024, 5, 1, 23)); | |
| expect(eastLate.getDate()).toBe(2); | |
| expect(getSemanticVersion(eastLate)).toBe("2024.6.1"); | |
| } finally { | |
| if (originalTimeZone === undefined) delete process.env.TZ; | |
| else process.env.TZ = originalTimeZone; | |
| } | |
| }); | |
| }); | |
| describe("formatRelativeTime", () => { | |
| // The function reads Date.now() itself, so under real time the ms between the | |
| // test's Date.now() and the function's can only push a fixture further into | |
| // the past, out of its bucket for any row sitting at a bucket's top edge. | |
| // A frozen clock puts every row exactly on its cutoff. | |
| beforeEach(() => { | |
| vi.useFakeTimers(); | |
| vi.setSystemTime(new Date("2024-06-15T12:00:00Z")); | |
| }); | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| }); | |
| // Each row sits at a branch cutoff, where an off-by-one would flip the | |
| // output to the adjacent form. | |
| it.each([ | |
| ["the current instant is Just now", 0, "Just now"], | |
| ["the last ms under a minute is still Just now", 59_999, "Just now"], | |
| ["one minute rolls up to 1m ago", 60_000, "1m ago"], | |
| ["59 minutes stays in minutes", 59 * 60_000, "59m ago"], | |
| ["60 minutes rolls up to an hour", 60 * 60_000, "1h ago"], | |
| ["90 minutes truncates to the hour", 90 * 60_000, "1h ago"], | |
| ["23 hours stays in hours", 23 * 3_600_000, "23h ago"], | |
| ["24 hours rolls up to a day", 24 * 3_600_000, "1d ago"], | |
| ["36 hours truncates to the day", 36 * 3_600_000, "1d ago"], | |
| ["6 days stays in days", 6 * 86_400_000, "6d ago"], | |
| ])("%s", (_, offsetMs, expected) => { | |
| expect(formatRelativeTime(Date.now() - offsetMs)).toBe(expected); | |
| }); | |
| it("rolls up to the locale date at 7 days", () => { | |
| const sevenDaysAgo = Date.now() - 7 * 86_400_000; | |
| expect(formatRelativeTime(sevenDaysAgo)).toBe( | |
| new Date(sevenDaysAgo).toLocaleDateString(), | |
| ); | |
| }); | |
| }); | |
| describe("searchWithFuzzy", () => { | |
| const items = [ | |
| { id: 1, name: "apple" }, | |
| { id: 2, name: "application" }, | |
| { id: 3, name: "banana" }, | |
| { id: 4, name: "blueprint" }, | |
| ]; | |
| it("should return all items with score 0 for empty query", () => { | |
| const results = searchWithFuzzy(items, "", (item) => item.name); | |
| expect(results).toHaveLength(4); | |
| expect(results.every((r) => r.score === 0)).toBe(true); | |
| }); | |
| it("should find matches with fuzzy search", () => { | |
| const results = searchWithFuzzy(items, "app", (item) => item.name); | |
| expect(results.length).toBeGreaterThan(0); | |
| expect(results[0].item.name).toContain("app"); | |
| }); | |
| it("should respect limit parameter", () => { | |
| const results = searchWithFuzzy(items, "a", (item) => item.name, 2); | |
| expect(results).toHaveLength(2); | |
| }); | |
| it("should return empty array for no matches", () => { | |
| const results = searchWithFuzzy(items, "xyz", (item) => item.name); | |
| expect(results).toHaveLength(0); | |
| }); | |
| it("should rank the matches and score them by rank", () => { | |
| // Listed with the weaker match first, so document order cannot produce | |
| // the expected result: uFuzzy's ranking has to be what orders these. | |
| const results = searchWithFuzzy( | |
| [ | |
| { id: 2, name: "application" }, | |
| { id: 1, name: "apple" }, | |
| { id: 3, name: "banana" }, | |
| ], | |
| "app", | |
| (item) => item.name, | |
| ); | |
| expect(results.map(({ item, score }) => [item.name, score])).toEqual([ | |
| ["apple", 1], | |
| ["application", 0.5], | |
| ]); | |
| }); | |
| }); | |
| describe("groupSearchResultsByDate", () => { | |
| // The buckets are calendar days, so every fixture below is relative to a | |
| // clock. Pinned to local midday: under real time, a run starting seconds | |
| // after midnight puts timestamps minutes apart on different days. | |
| beforeEach(() => { | |
| vi.useFakeTimers(); | |
| vi.setSystemTime(new Date(2024, 5, 15, 12, 0, 0)); | |
| }); | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| }); | |
| it("should group items by Yesterday", () => { | |
| const yesterday = Date.now() - 24 * 60 * 60 * 1000; | |
| const items = [{ item: { id: 1 }, timestamp: yesterday }]; | |
| const groups = groupSearchResultsByDate(items); | |
| expect(groups.Yesterday).toHaveLength(1); | |
| }); | |
| it("should group items by This Week", () => { | |
| const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000; | |
| const items = [{ item: { id: 1 }, timestamp: threeDaysAgo }]; | |
| const groups = groupSearchResultsByDate(items); | |
| expect(groups["This Week"]).toHaveLength(1); | |
| }); | |
| it("should group older items by month", () => { | |
| const oldTimestamp = Date.now() - 30 * 24 * 60 * 60 * 1000; | |
| const items = [{ item: { id: 1 }, timestamp: oldTimestamp }]; | |
| const groups = groupSearchResultsByDate(items); | |
| const keys = Object.keys(groups); | |
| expect(keys.length).toBe(1); | |
| expect(keys[0]).toMatch(/^\w{3} \d{4}$/); | |
| }); | |
| it("should handle empty array", () => { | |
| const groups = groupSearchResultsByDate([]); | |
| expect(Object.keys(groups)).toHaveLength(0); | |
| }); | |
| it("should group multiple items in same category", () => { | |
| const now = Date.now(); | |
| const items = [ | |
| { item: { id: 1 }, timestamp: now }, | |
| { item: { id: 2 }, timestamp: now - 60 * 1000 }, | |
| ]; | |
| const groups = groupSearchResultsByDate(items); | |
| expect(groups.Today).toHaveLength(2); | |
| }); | |
| }); | |
| }); | |