Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions apps/mobile/src/features/tasks/components/TaskItem.test.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => createElement("Check", props),
GitPullRequest: (props: Record<string, unknown>) =>
createElement("GitPullRequest", props),
}));

vi.mock("@/lib/theme", () => ({
useThemeColors: () => ({
gray: { 11: "#444444" },
accent: { 9: "#ff5500" },
}),
}));

vi.mock("@components/text", () => ({
Text: (props: Record<string, unknown>) => createElement("Text", props),
}));

vi.mock("./TaskStatusIcon", () => ({
TaskStatusIcon: (props: Record<string, unknown>) =>
createElement("TaskStatusIcon", props),
}));

function makeTask(run?: Partial<NonNullable<Task["latest_run"]>>): 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<typeof create>;
act(() => {
renderer = create(createElement(TaskItem, { task, onPress: () => {} }));
});
return renderer;
}

describe("TaskItem", () => {
function prIcons(renderer: ReturnType<typeof create>) {
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);
});
});
Comment on lines +65 to +106
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests for the "no badge" cases

The team rule is to prefer parameterised tests. The single "does not show the PR badge when there is no PR url" case only exercises latest_run: undefined. Other no-badge paths — output: null, a GitHub issue URL (/issues/42), or a non-GitHub URL — are untested and aren't obviously covered by the parser's own suite. A it.each table would cover all of these without duplicating the assertion logic.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/tasks/components/TaskItem.test.tsx
Line: 64-90

Comment:
**Prefer parameterised tests for the "no badge" cases**

The team rule is to prefer parameterised tests. The single "does not show the PR badge when there is no PR url" case only exercises `latest_run: undefined`. Other no-badge paths — `output: null`, a GitHub *issue* URL (`/issues/42`), or a non-GitHub URL — are untested and aren't obviously covered by the parser's own suite. A `it.each` table would cover all of these without duplicating the assertion logic.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

33 changes: 28 additions & 5 deletions apps/mobile/src/features/tasks/components/TaskItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable
onPress={() => 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}`}
>
<GitPullRequest size={11} weight="bold" color={themeColors.gray[11]} />
<Text className="text-[11px] text-gray-11">{`#${number}`}</Text>
</Pressable>
);
}

interface TaskItemProps {
task: Task;
onPress: (task: Task) => void;
Expand All @@ -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 (
<Pressable
Expand Down Expand Up @@ -61,9 +80,13 @@ function TaskItemComponent({
>
{task.title}
</Text>
<Text className="shrink-0 text-[11px] text-gray-9">
{timeDisplay}
</Text>
{prRef?.kind === "pr" ? (
<PrBadge prUrl={prRef.normalizedUrl} number={prRef.number} />
) : (
<Text className="shrink-0 text-[11px] text-gray-9">
{timeDisplay}
</Text>
)}
</View>

{task.description ? (
Expand Down
Loading