diff --git a/apps/mobile/src/features/tasks/components/TaskItem.test.tsx b/apps/mobile/src/features/tasks/components/TaskItem.test.tsx new file mode 100644 index 0000000000..d62cfd7665 --- /dev/null +++ b/apps/mobile/src/features/tasks/components/TaskItem.test.tsx @@ -0,0 +1,106 @@ +import { createElement } from "react"; +import { act, create } from "react-test-renderer"; +import { describe, expect, it, vi } from "vitest"; +import type { Task } from "../types"; +import { TaskItem } from "./TaskItem"; + +vi.mock("phosphor-react-native", () => ({ + Check: (props: Record) => createElement("Check", props), + GitPullRequest: (props: Record) => + createElement("GitPullRequest", props), +})); + +vi.mock("@/lib/theme", () => ({ + useThemeColors: () => ({ + gray: { 11: "#444444" }, + accent: { 9: "#ff5500" }, + }), +})); + +vi.mock("@components/text", () => ({ + Text: (props: Record) => createElement("Text", props), +})); + +vi.mock("./TaskStatusIcon", () => ({ + TaskStatusIcon: (props: Record) => + createElement("TaskStatusIcon", props), +})); + +function makeTask(run?: Partial>): Task { + return { + id: "task-1", + task_number: 1, + slug: "task-1", + title: "Test task", + description: "", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + origin_product: "code", + latest_run: run + ? { + id: "run-1", + task: "task-1", + team: 1, + branch: null, + stage: null, + environment: "cloud", + status: "completed", + log_url: "", + error_message: null, + output: null, + state: {}, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + completed_at: null, + ...run, + } + : undefined, + }; +} + +function render(task: Task) { + let renderer!: ReturnType; + act(() => { + renderer = create(createElement(TaskItem, { task, onPress: () => {} })); + }); + return renderer; +} + +describe("TaskItem", () => { + function prIcons(renderer: ReturnType) { + return renderer.root.findAll( + (node) => String(node.type) === "GitPullRequest", + ); + } + + it("shows the PR badge with the parsed number when a PR url is present", () => { + const renderer = render( + makeTask({ + output: { pr_url: "https://github.com/PostHog/code/pull/2422" }, + }), + ); + + expect(prIcons(renderer)).toHaveLength(1); + const number = renderer.root.findAll( + (node) => String(node.type) === "Text" && node.props.children === "#2422", + ); + expect(number).toHaveLength(1); + }); + + it.each([ + ["the task has no run", makeTask()], + ["the run has no output", makeTask({ output: null })], + [ + "the url is a GitHub issue, not a PR", + makeTask({ + output: { pr_url: "https://github.com/PostHog/code/issues/42" }, + }), + ], + [ + "the url is not a GitHub url", + makeTask({ output: { pr_url: "https://example.com/not-a-pr" } }), + ], + ])("does not show the PR badge when %s", (_label, task) => { + expect(prIcons(render(task))).toHaveLength(0); + }); +}); diff --git a/apps/mobile/src/features/tasks/components/TaskItem.tsx b/apps/mobile/src/features/tasks/components/TaskItem.tsx index 715c70ebb0..e99bdfb768 100644 --- a/apps/mobile/src/features/tasks/components/TaskItem.tsx +++ b/apps/mobile/src/features/tasks/components/TaskItem.tsx @@ -1,12 +1,29 @@ import { Text } from "@components/text"; import { differenceInHours, format, formatDistanceToNow } from "date-fns"; -import { Check } from "phosphor-react-native"; +import { Check, GitPullRequest } from "phosphor-react-native"; import { memo } from "react"; -import { Pressable, View } from "react-native"; +import { Linking, Pressable, View } from "react-native"; +import { parseGithubIssueUrl } from "@/lib/githubIssueUrl"; import { useThemeColors } from "@/lib/theme"; import type { Task } from "../types"; import { TaskStatusIcon } from "./TaskStatusIcon"; +function PrBadge({ prUrl, number }: { prUrl: string; number: number }) { + const themeColors = useThemeColors(); + return ( + Linking.openURL(prUrl).catch(() => {})} + hitSlop={8} + className="shrink-0 flex-row items-center gap-0.5 rounded border border-gray-6 bg-gray-3 px-1.5 py-0.5 active:opacity-60" + accessibilityRole="link" + accessibilityLabel={`Open pull request #${number}`} + > + + {`#${number}`} + + ); +} + interface TaskItemProps { task: Task; onPress: (task: Task) => void; @@ -29,6 +46,8 @@ function TaskItemComponent({ hoursSinceCreated < 24 ? formatDistanceToNow(createdAt, { addSuffix: true }) : format(createdAt, "MMM d"); + const prUrl = task.latest_run?.output?.pr_url; + const prRef = typeof prUrl === "string" ? parseGithubIssueUrl(prUrl) : null; return ( {task.title} - - {timeDisplay} - + {prRef?.kind === "pr" ? ( + + ) : ( + + {timeDisplay} + + )} {task.description ? (