diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fbcf46..cb9c8b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,4 +21,4 @@ jobs: run: bun install --frozen-lockfile - name: Run tests - run: bun test + run: bun run test diff --git a/bun.lock b/bun.lock index 660b732..6dd9301 100644 --- a/bun.lock +++ b/bun.lock @@ -178,10 +178,15 @@ "name": "@aelis/source-tfl", "version": "0.0.0", "dependencies": { + "@aelis/components": "workspace:*", "@aelis/core": "workspace:*", "@aelis/source-location": "workspace:*", "arktype": "^2.1.0", }, + "peerDependencies": { + "@json-render/core": "*", + "@nym.sh/jrx": "*", + }, }, "packages/aelis-source-weatherkit": { "name": "@aelis/source-weatherkit", diff --git a/packages/aelis-source-tfl/package.json b/packages/aelis-source-tfl/package.json index 579f415..54b3297 100644 --- a/packages/aelis-source-tfl/package.json +++ b/packages/aelis-source-tfl/package.json @@ -10,7 +10,12 @@ }, "dependencies": { "@aelis/core": "workspace:*", + "@aelis/components": "workspace:*", "@aelis/source-location": "workspace:*", "arktype": "^2.1.0" + }, + "peerDependencies": { + "@json-render/core": "*", + "@nym.sh/jrx": "*" } } diff --git a/packages/aelis-source-tfl/src/index.ts b/packages/aelis-source-tfl/src/index.ts index f445054..08fdf35 100644 --- a/packages/aelis-source-tfl/src/index.ts +++ b/packages/aelis-source-tfl/src/index.ts @@ -11,3 +11,4 @@ export { type TflLineStatus, type TflSourceOptions, } from "./types.ts" +export { renderTflAlert } from "./renderer.tsx" diff --git a/packages/aelis-source-tfl/src/renderer.test.tsx b/packages/aelis-source-tfl/src/renderer.test.tsx new file mode 100644 index 0000000..ae3752a --- /dev/null +++ b/packages/aelis-source-tfl/src/renderer.test.tsx @@ -0,0 +1,103 @@ +/** @jsxImportSource @nym.sh/jrx */ +import { render } from "@nym.sh/jrx" +import { describe, expect, test } from "bun:test" + +import type { TflAlertFeedItem } from "./types.ts" + +import { renderTflAlert } from "./renderer.tsx" + +function makeItem(overrides: Partial = {}): TflAlertFeedItem { + return { + id: "tfl-alert-northern-minor-delays", + type: "tfl-alert", + timestamp: new Date("2026-01-15T12:00:00Z"), + data: { + line: "northern", + lineName: "Northern", + severity: "minor-delays", + description: "Minor delays due to signal failure", + closestStationDistance: null, + ...overrides, + }, + } +} + +describe("renderTflAlert", () => { + test("renders a FeedCard with title and description", () => { + const node = renderTflAlert(makeItem()) + const spec = render(node) + + const root = spec.elements[spec.root]! + expect(root.type).toBe("FeedCard") + expect(root.children!.length).toBeGreaterThanOrEqual(2) + + const title = spec.elements[root.children![0]!]! + expect(title.type).toBe("SansSerifText") + expect(title.props.content).toBe("Northern · Minor delays") + + const body = spec.elements[root.children![1]!]! + expect(body.type).toBe("SansSerifText") + expect(body.props.content).toBe("Minor delays due to signal failure") + }) + + test("shows nearest station distance when available", () => { + const node = renderTflAlert(makeItem({ closestStationDistance: 0.35 })) + const spec = render(node) + + const root = spec.elements[spec.root]! + expect(root.children).toHaveLength(3) + + const caption = spec.elements[root.children![2]!]! + expect(caption.type).toBe("SansSerifText") + expect(caption.props.content).toBe("Nearest station: 350m away") + }) + + test("formats distance in km when >= 1km", () => { + const node = renderTflAlert(makeItem({ closestStationDistance: 2.456 })) + const spec = render(node) + + const root = spec.elements[spec.root]! + const caption = spec.elements[root.children![2]!]! + expect(caption.props.content).toBe("Nearest station: 2.5km away") + }) + + test("formats near-1km boundary as km not meters", () => { + const node = renderTflAlert(makeItem({ closestStationDistance: 0.9999 })) + const spec = render(node) + + const root = spec.elements[spec.root]! + const caption = spec.elements[root.children![2]!]! + expect(caption.props.content).toBe("Nearest station: 1.0km away") + }) + + test("omits station distance when null", () => { + const node = renderTflAlert(makeItem({ closestStationDistance: null })) + const spec = render(node) + + const root = spec.elements[spec.root]! + // Title + body only, no caption (empty fragment doesn't produce a child) + const children = root.children!.filter((key) => { + const el = spec.elements[key] + return el && el.type !== "Fragment" + }) + expect(children).toHaveLength(2) + }) + + test("renders closure severity label", () => { + const node = renderTflAlert(makeItem({ severity: "closure", lineName: "Central" })) + const spec = render(node) + + const root = spec.elements[spec.root]! + const title = spec.elements[root.children![0]!]! + expect(title.props.content).toBe("Central · Closed") + }) + + test("renders major delays severity label", () => { + const node = renderTflAlert(makeItem({ severity: "major-delays", lineName: "Jubilee" })) + const spec = render(node) + + const root = spec.elements[spec.root]! + const title = spec.elements[root.children![0]!]! + expect(title.props.content).toBe("Jubilee · Major delays") + }) +}) diff --git a/packages/aelis-source-tfl/src/renderer.tsx b/packages/aelis-source-tfl/src/renderer.tsx new file mode 100644 index 0000000..e0eb02a --- /dev/null +++ b/packages/aelis-source-tfl/src/renderer.tsx @@ -0,0 +1,40 @@ +/** @jsxImportSource @nym.sh/jrx */ +import type { FeedItemRenderer } from "@aelis/core" + +import { FeedCard, SansSerifText } from "@aelis/components" + +import type { TflAlertData } from "./types.ts" + +import { TflAlertSeverity } from "./types.ts" + +const SEVERITY_LABEL: Record = { + [TflAlertSeverity.Closure]: "Closed", + [TflAlertSeverity.MajorDelays]: "Major delays", + [TflAlertSeverity.MinorDelays]: "Minor delays", +} + +function formatDistance(km: number): string { + const meters = Math.round(km * 1000) + if (meters < 1000) { + return `${meters}m away` + } + return `${(meters / 1000).toFixed(1)}km away` +} + +export const renderTflAlert: FeedItemRenderer<"tfl-alert", TflAlertData> = (item) => { + const { lineName, severity, description, closestStationDistance } = item.data + const severityLabel = SEVERITY_LABEL[severity] + + return ( + + + + {closestStationDistance !== null ? ( + + ) : null} + + ) +} diff --git a/packages/aelis-source-tfl/tsconfig.json b/packages/aelis-source-tfl/tsconfig.json new file mode 100644 index 0000000..8d65648 --- /dev/null +++ b/packages/aelis-source-tfl/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsxImportSource": "@nym.sh/jrx" + }, + "include": ["src"] +}