Skip to content
Merged
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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
~~Please run `pnpm verify` before commit~~

# Session 1
- [x] Formatter -> Prettier
- [x] Git Hook -> Husky
Expand All @@ -10,7 +8,6 @@
- [x] Type-check -> CI Workflow
- [x] Linter
- [x] Linter -> CI Workflow

- [x] Deno Type-check
- [x] Deno Type-check -> CI Workflow
- [x] Deno formatter
Expand All @@ -23,8 +20,14 @@
- [x] Test runner -> CI Workflow (Frontend)

# Session 4
- [ ] Component test
- [ ] Component test -> CI Workflow
- [x] Testing Library
- [x] JSDom
- [x] Test Doubles
- [x] Test components
- [ ] Test hooks
- [ ] Mock
- [ ] Mock API calls
- [x] Component test -> CI Workflow

# Session 5
- [ ] UI Regression test
Expand Down
6 changes: 5 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { defineConfig } from "@fullstacksjs/eslint-config";

export default defineConfig({
typescript: {
projectService: {
allowDefaultProject: ["vitest.setup.ts"],
},
},
rules: {
"vitest/prefer-lowercase-title": "off",
"vitest/require-top-level-describe": "off",
},
});
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@
"devDependencies": {
"@fullstacksjs/eslint-config": "13.4.0",
"@tailwindcss/vite": "4.1.14",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/node": "24.8.1",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-react": "5.0.4",
"eslint": "9.37.0",
"jsdom": "27.1.0",
"prettier": "3.6.2",
"tailwindcss": "4.1.14",
"typescript": "5.9.3",
Expand Down
549 changes: 542 additions & 7 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions src/components/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,13 @@ export function Column({ column, tasks }: ColumnProps) {
deleteTaskMutation.mutate({ id: taskId })
}
onUpdate={(params) => {
const currentTask = tasks.find(
(t) => t.id === params.taskId,
);
const currentTask = tasks.find((t) => t.id === params.id);
if (currentTask) {
updateTaskMutation.mutate({
id: params.taskId,
title: params.updates.title || currentTask.title,
id: params.id,
title: params.title || currentTask.title,
description:
params.updates.description ||
currentTask.description ||
"",
params.description || currentTask.description || "",
});
}
}}
Expand Down
53 changes: 53 additions & 0 deletions src/components/TaskCard.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { screen } from "@testing-library/react";

import type { Task } from "../lib/apiClient";

import { customRender } from "../lib/test";
import { TaskCard } from "./TaskCard";

const task: Task = Object.freeze({
column_id: "1",
id: "task-1",
title: "Sample Task",
description: "This is a description.",
created_at: new Date("2023-01-01").toISOString(),
order_index: 0,
});

describe("taskCard", () => {
it("should should render title in h4", () => {
customRender(<TaskCard task={task} />);
const title = screen.getByText(task.title);

expect(title).toBeVisible();
});

it("should go to edit mode when title is clicked", async () => {
const { user } = customRender(<TaskCard task={task} />);
const title = screen.getByText(task.title);
await user.click(title);
const input = screen.getByDisplayValue(task.title);

expect(input).toBeVisible();
});

it("should call onUpdate when clicked on submit icon", async () => {
const spy1 = vi.fn();

const { user } = customRender(<TaskCard task={task} onUpdate={spy1} />);
const title = screen.getByText(task.title);
await user.click(title);
const input = screen.getByDisplayValue(task.title);
await user.clear(input);
await user.type(input, "Updated Task Title");

const button = screen.getByRole("button", { name: "Confirm changes" });
await user.click(button);

expect(spy1).toHaveBeenCalledExactlyOnceWith({
id: task.id,
title: "Updated Task Title",
description: task.description,
});
});
});
38 changes: 18 additions & 20 deletions src/components/TaskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Check, Edit3, Trash2 } from "lucide-react";
import { useState } from "react";
import { useRef, useState } from "react";

import type { Task } from "../lib/apiClient";

Expand All @@ -12,16 +12,16 @@ import { Textarea } from "../ui/Textarea";

interface Props {
task: Task;
onUpdate?: (params: { taskId: string; updates: Partial<Task> }) => void;
onUpdate?: (a: { id: string; title: string; description: string }) => void;
onDelete?: (taskId: string) => void;
isOverlay?: boolean;
}

// eslint-disable-next-line max-lines-per-function
export function TaskCard({ task, onUpdate, onDelete, isOverlay }: Props) {
const [isEditing, setIsEditing] = useState(false);
const [title, setTitle] = useState(task.title);
const [description, setDescription] = useState(task.description || "");
const ref = useRef<HTMLInputElement>(null);

const {
attributes,
Expand All @@ -40,23 +40,20 @@ export function TaskCard({ task, onUpdate, onDelete, isOverlay }: Props) {
opacity: isDragging ? 0.5 : 1,
};

const handleTitleSave = () => {
if (title.trim()) {
onUpdate?.({ taskId: task.id, updates: { title: title.trim() } });
setIsEditing(false);
}
};
const handleConfirm = () => {
if (title.trim() === "") return;

const handleDescriptionSave = () => {
onUpdate?.({
taskId: task.id,
updates: { description: description.trim() },
title: title.trim(),
id: task.id,
description: description.trim(),
});
setIsEditing(false);
};

const handleDescriptionKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleDescriptionSave();
handleConfirm();
} else if (e.key === "Escape") {
setDescription(task.description || "");
setIsEditing(false);
Expand All @@ -65,18 +62,13 @@ export function TaskCard({ task, onUpdate, onDelete, isOverlay }: Props) {

const handleTitleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleTitleSave();
handleConfirm();
} else if (e.key === "Escape") {
setTitle(task.title);
setIsEditing(false);
}
};

const handleConfirm = () => {
handleTitleSave();
handleDescriptionSave();
};

if (isOverlay) {
return (
<Card className="w-72">
Expand Down Expand Up @@ -105,6 +97,7 @@ export function TaskCard({ task, onUpdate, onDelete, isOverlay }: Props) {
<div className="space-y-2">
<Input
className="text-sm"
ref={ref}
type="text"
value={title}
autoFocus
Expand All @@ -125,7 +118,12 @@ export function TaskCard({ task, onUpdate, onDelete, isOverlay }: Props) {
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<h4
className="font-medium text-gray-900 text-sm cursor-pointer hover:bg-gray-50 py-1 px-2 rounded"
onClick={() => setIsEditing(true)}
onClick={() => {
setIsEditing(true);
setTimeout(() => {
ref.current?.setSelectionRange(0, 100);
}, 0);
}}
>
{task.title}
</h4>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

export const customRender = (...arg: Parameters<typeof render>) => {
const handle = render(...arg);
return { ...handle, user: userEvent.setup() };
};
6 changes: 5 additions & 1 deletion tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"types": ["vite/client"]
"types": [
"vite/client",
"@testing-library/jest-dom/vitest",
"vitest/globals"
]
},
"include": ["src"]
}
6 changes: 6 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/// <reference types="vitest" />
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
},
});
1 change: 1 addition & 0 deletions vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";