Skip to content

feat: add vapor-unit-test skills#620

Open
noahchoii wants to merge 4 commits into
mainfrom
unit-test-skills
Open

feat: add vapor-unit-test skills#620
noahchoii wants to merge 4 commits into
mainfrom
unit-test-skills

Conversation

@noahchoii

@noahchoii noahchoii commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

Summary by CodeRabbit

  • Documentation
    • Added new end-to-end unit testing guidelines for the monorepo using Vitest, React Testing Library, and related conventions (including coverage targets and test structure rules).
    • Added reference guides for React component tests, React hook tests, and general JS/TS testing.
    • Added example test suites for a Breadcrumb component (accessibility + keyboard behavior), a useInterval hook (fake timers + cleanup), and stateful-props utilities (merging/resolution behavior).

Summary

Adds vapor-unit-test skill — convention guide for writing Vitest + React Testing Library
unit tests across the vapor-ui monorepo. Triggered by phrases like "테스트 추가해줘", "write
unit tests", "커버리지 보강", "test 좀 짜줘" — no explicit skill invocation required.

Structure

Progressive disclosure: skill body stays short, dispatches to a single reference per target.

  vapor-unit-test/
  ├── SKILL.md                          # routing + rules common to all targets
  └── references/
      ├── component-tests.md            # React components
      ├── hook-tests.md                 # custom hooks
      ├── general-tests.md              # pure JS/TS (functions, transforms, lint rules…)
      └── examples/
          ├── breadcrumb.test.tsx
          ├── use-hook.test.ts
          └── util.test.ts

SKILL.md carries the routing table + rules that apply regardless of target (file
placement, fixture layout, imports, naming, mocks, coverage). Each reference is loaded only
when its target matches — keeps Claude's context lean.

Decisions

  • Three targets, three references — component / hook / general JS-TS. Each has
    materially different assertion vocabulary (RTL queries vs renderHook/act vs
    input→output), and bundling them into one doc would force Claude to read irrelevant guidance
    on every invocation.
  • Examples as separate files, not inline — full runnable test files survive scrolling
    and let Claude pattern-match on real shapes.
  • Co-location by default, __tests__/ for cross-file flows — keeps single-file targets
    discoverable; isolates multi-file integration scenarios where co-location would clutter the
    source folder.
  • Fixtures live under __testfixtures__/ — single glob name (jscodeshift convention)
    used across all domains so Vitest/tsconfig can exclude them in one place; prevents
    partial-source fixtures from breaking build/typecheck.
  • Coverage gate: 80% across four indicators — single explicit threshold, with a
    delegation rule (one-line forward to external lib = skip) so the guide doesn't push toward
    testing third-party code.
  • TDD optional — primary use case is writing tests against existing code; forcing
    red→green→refactor would conflict with how the skill is actually invoked.
  • Visual props excludedsize/variant/color/shape belong to visual regression;
    declaring this up front prevents redundant unit tests.

Screenshots

Checklist

Before submitting the PR, please make sure you have checked all of the following items.

  • The PR title follows the Conventional Commits convention. (e.g., feat, fix, docs, style, refactor, test, chore)
  • I have added tests for my changes.
  • I have updated the Storybook or relevant documentation.
  • I have added a changeset for this change. (e.g., for any changes that affect users, such as component prop changes or new features).
  • I have performed a self-code review.
  • I have followed the project's coding conventions and component patterns.

@noahchoii noahchoii requested a review from MaxLee-dev as a code owner June 19, 2026 01:32
@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f5f0cae

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vapor-ui Ready Ready Preview, Comment Jul 7, 2026 6:20am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e1edf824-debd-4fbb-8430-a21d00c37f71

📥 Commits

Reviewing files that changed from the base of the PR and between 735c98a and f5f0cae.

📒 Files selected for processing (1)
  • .claude/skills/vapor-unit-test/references/component-tests.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude/skills/vapor-unit-test/references/component-tests.md

📝 Walkthrough

Walkthrough

Adds a new .claude/skills/vapor-unit-test skill directory with a top-level workflow guide, three testing reference documents, and three example test files covering component, hook, and general JS/TS Vitest conventions.

Changes

vapor-unit-test AI Skill

Layer / File(s) Summary
Top-level skill workflow and conventions
.claude/skills/vapor-unit-test/SKILL.md
Defines the entry-point workflow, reference selection rules, test placement guidance, Vitest global import rules, coverage targets, and the pre-start checklist.
Component testing guide and Breadcrumb example
.claude/skills/vapor-unit-test/references/component-tests.md, .claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx
Defines component test structure, query and assertion conventions, accessibility checks, user-event usage, state assertions, callback scope rules, and wrapper patterns; the breadcrumb example applies them in accessibility, ARIA, keyboard, and current-state tests.
Hook testing guide and useInterval example
.claude/skills/vapor-unit-test/references/hook-tests.md, .claude/skills/vapor-unit-test/references/examples/use-hook.test.ts
Defines hook test structure, renderHook and rerender usage, act wrapping, fake timer patterns, cleanup checks, context wrappers, and test scope; the hook example covers interval firing, null delays, rerenders, unmount cleanup, and callback updates.
General JS/TS testing guide and stateful-props example
.claude/skills/vapor-unit-test/references/general-tests.md, .claude/skills/vapor-unit-test/references/examples/util.test.ts
Defines non-React JS/TS test conventions and links them to a utility example covering mergeStatefulProps, resolveClassName, and resolveStyle behavior.
Estimated code review effort: 2 (Simple) ~12 minutes
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding the vapor-unit-test skill documentation and references.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch unit-test-skills

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
.claude/skills/vapor-unit-test/references/examples/util.test.ts (1)

45-48: ⚡ Quick win

Tighten the invalid-input assertion to avoid false positives.

Line 48 uses a generic .toThrow(), which can pass on unrelated exceptions. Assert an expected message/pattern so the test validates the intended guard path.

Suggested patch
-            ).toThrow();
+            ).toThrow(/string|function/i);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/vapor-unit-test/references/examples/util.test.ts around lines
45 - 48, The test for resolveClassName at line 45-48 uses a generic `.toThrow()`
assertion without specifying an expected error message or pattern, which can
pass if any exception is thrown rather than validating the specific guard
condition being tested. Add an expected error message string or regex pattern as
an argument to the `.toThrow()` call to validate that the error thrown is
specifically for the invalid resolver input check (when the resolver is neither
a string nor a function), ensuring the test validates the intended error path
rather than accepting unrelated exceptions.
.claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx (1)

44-50: ⚡ Quick win

Make the prop: current block actually validate prop-driven behavior.

The current test repeats the same positive assertion as the ARIA block, but doesn’t exercise the prop boundary (e.g., current absent vs present). This weakens the value of the “prop: current” section.

Suggested patch
-    describe('prop: current', () => {
-        it('sets aria-current="page" on the active item', () => {
-            const rendered = render(<BreadcrumbTest />);
-            const currentItem = rendered.getByRole('link', { name: 'Products' });
-
-            expect(currentItem).toHaveAttribute('aria-current', 'page');
-        });
-    });
+    describe('prop: current', () => {
+        it('sets `aria-current="page"` when current is true', () => {
+            const rendered = render(<BreadcrumbTest current />);
+            const currentItem = rendered.getByRole('link', { name: 'Products' });
+            expect(currentItem).toHaveAttribute('aria-current', 'page');
+        });
+    });
@@
-const BreadcrumbTest = (props: Breadcrumb.Root.Props) => (
+const BreadcrumbTest = ({ current = false, ...props }: Breadcrumb.Root.Props & { current?: boolean }) => (
     <Breadcrumb.Root {...props}>
         <Breadcrumb.Item href="home">Home</Breadcrumb.Item>
         <Breadcrumb.Separator />
-        <Breadcrumb.Item href="products" current>
+        <Breadcrumb.Item href="products" current={current}>
             Products
         </Breadcrumb.Item>
     </Breadcrumb.Root>
 );

Also applies to: 54-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx
around lines 44 - 50, The test within the describe block labeled "prop: current"
is repeating the same positive assertion as the ARIA block instead of validating
prop-driven behavior. Refactor this test to exercise the boundary of the current
prop by testing what happens when the current prop is present versus absent.
This should demonstrate how the component behaves differently based on whether
the current prop is passed, rather than just asserting the same positive case
that is already covered elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx:
- Around line 25-34: In the keyboard navigation test (the 'should support
keyboard navigation via Tab and Enter' test), the render call is not following
the documented convention. Refactor the render statement to assign the result to
a const variable named rendered (i.e., const rendered = render(...)) instead of
calling render directly without assignment. This ensures the example follows the
canonical pattern documented in the guide.

In @.claude/skills/vapor-unit-test/SKILL.md:
- Around line 64-71: The fenced code block displaying the file tree structure
lacks a language identifier on the opening fence, which violates the
markdownlint MD040 rule. Add a language tag to the opening fence by changing ```
to ```text to indicate the content is plain text, which will satisfy the
markdown linting requirement and prevent pipeline failures.

---

Nitpick comments:
In @.claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx:
- Around line 44-50: The test within the describe block labeled "prop: current"
is repeating the same positive assertion as the ARIA block instead of validating
prop-driven behavior. Refactor this test to exercise the boundary of the current
prop by testing what happens when the current prop is present versus absent.
This should demonstrate how the component behaves differently based on whether
the current prop is passed, rather than just asserting the same positive case
that is already covered elsewhere.

In @.claude/skills/vapor-unit-test/references/examples/util.test.ts:
- Around line 45-48: The test for resolveClassName at line 45-48 uses a generic
`.toThrow()` assertion without specifying an expected error message or pattern,
which can pass if any exception is thrown rather than validating the specific
guard condition being tested. Add an expected error message string or regex
pattern as an argument to the `.toThrow()` call to validate that the error
thrown is specifically for the invalid resolver input check (when the resolver
is neither a string nor a function), ensuring the test validates the intended
error path rather than accepting unrelated exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2315e470-ea70-4af7-bb87-7f59f8e722a9

📥 Commits

Reviewing files that changed from the base of the PR and between 91cd85b and ae6af17.

📒 Files selected for processing (7)
  • .claude/skills/vapor-unit-test/SKILL.md
  • .claude/skills/vapor-unit-test/references/component-tests.md
  • .claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx
  • .claude/skills/vapor-unit-test/references/examples/use-hook.test.ts
  • .claude/skills/vapor-unit-test/references/examples/util.test.ts
  • .claude/skills/vapor-unit-test/references/general-tests.md
  • .claude/skills/vapor-unit-test/references/hook-tests.md

Comment on lines +25 to +34
it('should support keyboard navigation via Tab and Enter', async () => {
const onClick = vi.fn();
render(
<Breadcrumb.Root>
<Breadcrumb.Item href="home">Home</Breadcrumb.Item>
<Breadcrumb.Item href="away" onClick={onClick}>
Away
</Breadcrumb.Item>
</Breadcrumb.Root>,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Align the example with the rendered convention defined in the guide.

Line 27 bypasses the documented const rendered = render(...) pattern. Since this file is a canonical example, it should strictly follow the convention it teaches.

Suggested patch
-            render(
+            const rendered = render(
                 <Breadcrumb.Root>
                     <Breadcrumb.Item href="home">Home</Breadcrumb.Item>
                     <Breadcrumb.Item href="away" onClick={onClick}>
                         Away
                     </Breadcrumb.Item>
                 </Breadcrumb.Root>,
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should support keyboard navigation via Tab and Enter', async () => {
const onClick = vi.fn();
render(
<Breadcrumb.Root>
<Breadcrumb.Item href="home">Home</Breadcrumb.Item>
<Breadcrumb.Item href="away" onClick={onClick}>
Away
</Breadcrumb.Item>
</Breadcrumb.Root>,
);
it('should support keyboard navigation via Tab and Enter', async () => {
const onClick = vi.fn();
const rendered = render(
<Breadcrumb.Root>
<Breadcrumb.Item href="home">Home</Breadcrumb.Item>
<Breadcrumb.Item href="away" onClick={onClick}>
Away
</Breadcrumb.Item>
</Breadcrumb.Root>,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/vapor-unit-test/references/examples/breadcrumb.test.tsx
around lines 25 - 34, In the keyboard navigation test (the 'should support
keyboard navigation via Tab and Enter' test), the render call is not following
the documented convention. Refactor the render statement to assign the result to
a const variable named rendered (i.e., const rendered = render(...)) instead of
calling render directly without assignment. This ensures the example follows the
canonical pattern documented in the guide.

Comment on lines +64 to +71
```
<area>/
├── target.ts
├── target.test.ts
└── __testfixtures__/
├── case-a.input.tsx
└── case-a.output.tsx
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced example block.

Line 64 opens a fenced code block without a language, which triggers markdownlint MD040 and can fail doc-lint pipelines depending on config.

Suggested patch
-```
+```text
 <area>/
 ├── target.ts
 ├── target.test.ts
 └── __testfixtures__/
     ├── case-a.input.tsx
     └── case-a.output.tsx
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 64-64: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/vapor-unit-test/SKILL.md around lines 64 - 71, The fenced
code block displaying the file tree structure lacks a language identifier on the
opening fence, which violates the markdownlint MD040 rule. Add a language tag to
the opening fence by changing ``` to ```text to indicate the content is plain
text, which will satisfy the markdown linting requirement and prevent pipeline
failures.

Source: Linters/SAST tools

@MaxLee-dev MaxLee-dev left a comment

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.

I understand that the content you've provided serves as a convention for test cases. For this type of content, Anthropic officially recommends adding it under rules.

Since skills are only triggered based on the conditions defined in their descriptions, how about moving it to [rules], which are triggered based on file paths? Please let me know what you think!

});
```

`cleanup`은 RTL이 자동으로 호출하므로 **기본적으로 추가하지 않습니다.** 한 `it()` 내부에서 여러 번 `render`를 호출하는 등 특수한 경우에만 명시적으로 `afterEach(cleanup)` 또는 `cleanup()`을 직접 호출하세요.

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.

In this case, rather than manually calling cleanup within a single test, I think it would be more appropriate to separate the test cases.

Therefore, instead of guiding the use of cleanup as an example for special cases, how about updating the convention to recommend using rerender or separating the test cases?

Additionally, for edge cases where manual DOM cleanup is actually required, unmount is generally used much more often than cleanup. Since we are writing component-level tests, there will be scenarios where we need to test specific behaviors upon unmounting (e.g., event listener removal for a Sheet or Dialog). For these situations, specifically calling unmount() on the target component is much more desirable than using cleanup() to wipe out the entire global DOM!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cleanup은 내부적으로 unmount()를 포함하고 있기도 하고, 많이 쓰는 패턴으로 알고 있긴 합니다..! 그리고 afterEach(cleanup)이 의미가 있었던 이유는 매번 동일한 테스트베드를 호출하는 번거로움을 줄이기 위해 아래와 같이 작성하고 있기 때문이었습니다!

describe('xxx', () => {
    let rendered: RenderResult;

    beforeEach(() => {
        rendered = render(<TestBed />);
    })

    it('can test without render testbed', () => {
        const element = rendered.getByRole('textbox');
        // ...
    })
})

이 때 사전에 생성된 테스트베드 대신 새로운 케이스를 띄워야 하는 경우 DOM을 지우고 새롭게 render하기 위해 cleanup을 호출해야 했어요.

다만 제가 놓친 부분이 있어서 정리해서 제안드리자면 다음과 같습니다!

  1. vitest 설정 파일에 globals: true를 지정해주면 RTL 같은 라이브러리에 의해 DOM이 auto-cleanup 된다고 하네요! 그래서 afterEach(cleanup)은 더이상 필요없을 것 같아요.
  2. 그리고 제안해주신 특수 케이스에 대한 사용을 제거하기 위해서는 rendered = beforeEach()와 같은 사용법을 지양해야 하는데, 조금 번거롭긴 하겠지만 그래도 각 테스트케이스에서 사용할 테스트베드를 직접 설정해준다는 의미로 해당 가이드는 제거해두도록 하겠습니다!


## describe 그룹

테스트가 **2개 이상의 카테고리**로 나뉠 때만 nested describe로 묶습니다. 카테고리가 1개거나 테스트가 몇 개 안 되면 nested 없이 최상위 `describe`에 평탄하게 둡니다 — 불필요한 중첩은 가독성을 해칩니다.

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.

The phrasing in "카테고리가 1개거나 테스트가 몇 개 안 되면 nested 없이" feels a bit ambiguous.

How about updating it to "카테고리가 1개거나, 테스트 케이스의 케이스가 적어 카테고리를 분류할 수 없는 경우 nested 없이" for better clarity?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants