Skip to content
Closed
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
19 changes: 10 additions & 9 deletions .agents/skills/building-blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ A structured catalog of typed frontend building blocks, optimized for AI agents.

## Building Block Categories

| Category | Blocks | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------- |
| Data Fetching | 4 | Query factories, mutations, keys, DTOs |
| State Management | 2 | Zustand stores, React Context providers |
| App Orchestration | 1 | Use case hooks composing mutations + notifications |
| Component Patterns | 7 | Pure components, compound components, forms, pages, HOCs, facade hooks, named effects |
| Data Modeling | 2 | Frontend models, value objects |
| Category | Blocks | Description |
| ------------------- | ------ | ------------------------------------------------------------------------------------- |
| Data Fetching | 4 | Query factories, mutations, keys, DTOs |
| State Management | 2 | Zustand stores, React Context providers |
| App Orchestration | 1 | Use case hooks composing mutations + notifications |
| Component Patterns | 7 | Pure components, compound components, forms, pages, HOCs, facade hooks, named effects |
| Test Infrastructure | 4 | Vitest unit tests, Storybook play-function tests, MSW handlers, test data fixtures |
| Data Modeling | 2 | Frontend models, value objects |

## How It Works

Expand All @@ -35,8 +36,8 @@ A structured catalog of typed frontend building blocks, optimized for AI agents.
```markdown
---
title: Block Display Name
category: Data Fetching | State Management | App Orchestration | Component Patterns | Data Modeling
layer: providers/ | application/ | components/ | models/ | lib/api/ | pages/
category: Data Fetching | State Management | App Orchestration | Component Patterns | Test Infrastructure | Data Modeling
layer: providers/ | application/ | components/ | models/ | pages/ | lib/api/ | test-lib/handlers/ | test-lib/fixtures/ | co-located
composedWith: other-block-1, other-block-2
---

Expand Down
9 changes: 9 additions & 0 deletions .agents/skills/building-blocks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ Read the rule file for each block you are about to implement. The summaries belo
| `facade-hook` | `components/` | Private logic extraction for a single component. Defined below the component, not exported. | `rules/facade-hook.md` |
| `named-effect` | `components/` | Named function expressions in `useEffect`. Intent visible at a glance. | `rules/named-effect.md` |

### Test Infrastructure

| Block | Layer | Summary | File |
| ---------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `unit-test` | co-located `.test.ts(x)` | Vitest test for mechanics — mappers, transformers, value objects, custom hooks with non-trivial logic. | `rules/unit-test.md` |
| `component-story-test` | co-located `.stories.tsx` | Storybook play-function test verifying user-facing behavior. Primary verification layer for anything component-touching. | `rules/component-story-test.md` |
| `msw-handler` | `test-lib/handlers/` | One request handler per endpoint with optional resolver for per-scenario overrides. Shared across tests and stories. | `rules/msw-handler.md` |
| `fixture` | `test-lib/fixtures/` | Deterministic test data factory via `createFixture<T>()`. Supplies `toStructure`, `createPermutation`, `createCollection`. | `rules/fixture.md` |

### Data Modeling

| Block | Layer | Summary | File |
Expand Down
8 changes: 6 additions & 2 deletions .agents/skills/building-blocks/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "1.0.0",
"version": "1.1.0",
"date": "April 2026",
"abstract": "Typed catalog of frontend building blocks. Contains 15 blocks across 5 categories (Data Fetching, State Management, App Orchestration, Component Patterns, Data Modeling). Each block defines composition rules, layer constraints, and canonical implementation examples. Designed for AI agents — read the index to route, read the rule file to implement.",
"abstract": "Typed catalog of frontend building blocks. Contains 19 blocks across 6 categories (Data Fetching, State Management, App Orchestration, Component Patterns, Test Infrastructure, Data Modeling). Each block defines composition rules, layer constraints, and canonical implementation examples. Designed for AI agents — read the index to route, read the rule file to implement.",
"categories": [
{
"name": "Data Fetching",
Expand Down Expand Up @@ -32,6 +32,10 @@
"named-effect"
]
},
{
"name": "Test Infrastructure",
"blocks": ["unit-test", "component-story-test", "msw-handler", "fixture"]
},
{
"name": "Data Modeling",
"blocks": ["frontend-model", "value-object"]
Expand Down
139 changes: 139 additions & 0 deletions .agents/skills/building-blocks/rules/component-story-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: Component Story Test
category: Testing
layer: co-located `.stories.tsx`
composedWith: fixture, msw-handler
---

## Component Story Test

Storybook story with a `play` function that verifies component behavior through real user interactions — clicks, typing, selection, validation, conditional rendering. One story per meaningful state or flow; the `Default` story is the pure-rendering baseline.

### Constraints

- Co-located with the component. `FooForm.tsx` → `FooForm.stories.tsx`.
- `Default: Story = {}` is the pure-rendering baseline — no play function on `Default`. Named stories carry play functions.
- Play function phases are always wrapped in `step("label", async () => { ... })`. Phase labels read as prose ("Validate required fields", "Fill text fields", "Submit form").
- Query priorities (in order):
1. `getByRole(role, { name })` — accessible-name queries
2. `getByLabelText(label)` — form fields
3. `getByText(text)` — visible content
4. `getByTestId` — escape hatch only
- Use `canvas` (from `within(canvasElement)`) for elements rendered inside the component. Use `screen` for portaled content (Chakra UI combobox options, toasts, modals).
- `await screen.findBy*` for elements that appear after an interaction. `sleep(N)` only for Chakra UI combobox open/close transitions (typical: `100`–`150` ms).
- Callback props use `action("label")` from `storybook/actions`. Data in args uses fixtures (`ProductFixture.toStructure()`, `ProductFixture.createCollection([...])`) — not inline literals.
- Network-dependent stories declare MSW handlers under `parameters.msw.handlers`. Per-scenario overrides pass a resolver to the handler factory (see `rules/msw-handler.md`).
- New story vs. new step: new story when starting props/state differ; new step within the same story when continuing a single user flow.

### Example — pure rendering baseline

Follow the structure of this example (meta at top with `title`, `component`, `decorators`, `parameters`; `satisfies Meta<typeof Component>` — not a plain type annotation — to keep inference working for `StoryObj<typeof meta>`).

```tsx
import type { Meta, StoryObj } from "@storybook/react-vite";
import { withRouter } from "storybook-addon-remix-react-router";

import { ProductFixture } from "@/test-lib/fixtures/product-fixture";
import { generateUuid } from "@/test-lib/generate-uuid";

import { ProductsList } from "./ProductsList";

const meta = {
title: "modules/Products/ProductsList",
component: ProductsList,
decorators: [withRouter],
parameters: { layout: "centered" },
} satisfies Meta<typeof ProductsList>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
products: ProductFixture.createCollection([
{ id: generateUuid() },
{ id: generateUuid() },
{ id: generateUuid() },
]),
},
};

export const WithoutProducts: Story = {
args: { products: [] },
};
```

### Example — interaction flow with steps

```tsx
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within, screen, expect } from "storybook/test";

import { sleep } from "@/test-lib/storybook/sleep";

import { CheckoutForm } from "./CheckoutForm";

const meta = {
title: "modules/Carts/CheckoutForm",
component: CheckoutForm,
parameters: { layout: "centered" },
} satisfies Meta<typeof CheckoutForm>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Purchasing: Story = {
play: async ({ canvasElement, step }) => {
within(canvasElement);

await step("Enter credentials", async () => {
await userEvent.type(screen.getByLabelText(/Full Name/i), "John Doe");
await userEvent.type(
screen.getByLabelText(/Address/i),
"NYC Groove Street"
);
await userEvent.click(screen.getByRole("combobox"));
await sleep(100);
await userEvent.click(screen.getByRole("option", { name: "PayPal" }));
await sleep(100);
});

await expect(screen.getByRole("combobox")).toHaveTextContent("PayPal");

await step("Submit form", async () => {
await userEvent.click(
screen.getByRole("button", { name: "Complete Order" })
);
});

await expect(
await screen.findByText(
"You have successfully purchased all selected products."
)
).toBeInTheDocument();
},
};
```

### Example — overriding MSW at call site

```tsx
// In a story's parameters block — simulate a 500 error for this story only
parameters: {
msw: {
handlers: [
getProductsHandler(() =>
HttpResponse.json({ message: "Server error" }, { status: 500 })
),
],
},
}
```

### References

- `rules/msw-handler.md` — wire mocked endpoints under `parameters.msw.handlers`
- `rules/fixture.md` — deterministic data for `args`
- `rules/unit-test.md` — for pure-logic units (hooks, utilities) that do not require rendering
83 changes: 83 additions & 0 deletions .agents/skills/building-blocks/rules/fixture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: Fixture
category: Testing
layer: test-lib/fixtures/
composedWith: -
---

## Fixture

Deterministic test data factory for a domain type. Built via the shared `createFixture<T>(template)` helper, which returns an object with three methods: `toStructure()`, `createPermutation(extend?)`, and `createCollection(extend)`. Used in both unit tests and Storybook story args.

### Available methods

- **`toStructure()`** — returns a deep clone of the template. No overrides. Use when the default values are exactly what the test needs.
- **`createPermutation(extend?)`** — returns a single instance with a deep merge applied on top of the template. `extend` can be either a `DeepPartial<T>` object or a function `(template: T) => DeepPartial<T>` (useful when the override depends on a value from the template itself).
- **`createCollection(extend)`** — returns an array. `extend` is either an array of `DeepPartial<T>` (one entry per item) or a function `(template: T) => DeepPartial<T>[]` (again, when the overrides depend on the template). Each item is a deep merge of the template and its entry.

### Constraints

- One file per domain type. Filename mirrors the type: `product-fixture.ts` exports `ProductFixture`.
- Templates are deterministic: no `Math.random()`, no `Date.now()`, no `faker`. Use fixed strings, fixed ISO date strings, and `generateUuid()` (from `@/test-lib/generate-uuid`) when a UUID is part of the template.
- Uniqueness across a collection is the caller's responsibility: pass `{ id: generateUuid() }` per item when unique IDs matter.
- Fixtures live in `test-lib/fixtures/` — never in `src/features/`.

### Example — template definition

```ts
import { Category } from "@/features/products/models/category";
import type { Product } from "@/features/products/models/product";
import { generateUuid } from "@/test-lib/generate-uuid";

import { createFixture } from "./create-fixture";

export const ProductFixture = createFixture<Product>({
id: generateUuid(),
name: "White Nike Shoes",
category: Category.Clothing,
price: { amount: 129.99, currency: "USD" },
imageUrl: "https://example.com/white-nike-shoes.jpg",
description: "Lorem ipsum dolor sit amet.",
addedAt: "2025-01-15T10:00:00.000Z",
});
```

### Example — consumer usage

```ts
// Defaults only
const defaultProduct = ProductFixture.toStructure();

// Single instance with an object override
const blackShoes = ProductFixture.createPermutation({
name: "Black Adidas Shoes",
});

// Single instance with a function override (uses the template to derive the override)
const discounted = ProductFixture.createPermutation((template) => ({
price: {
amount: template.price.amount * 0.5,
currency: template.price.currency,
},
}));

// Collection with per-item overrides (unique IDs)
const products = ProductFixture.createCollection([
{ id: generateUuid() },
{ id: generateUuid() },
{ id: generateUuid(), name: "Special Edition" },
]);

// Collection with function override
const threeVariants = ProductFixture.createCollection((template) => [
{ id: generateUuid(), name: `${template.name} - S` },
{ id: generateUuid(), name: `${template.name} - M` },
{ id: generateUuid(), name: `${template.name} - L` },
]);
```

### References

- `rules/msw-handler.md` — fixtures populate default handler responses
- `rules/component-story-test.md` — fixtures populate story `args`
- `rules/unit-test.md` — fixtures replace inline literals in unit tests
68 changes: 68 additions & 0 deletions .agents/skills/building-blocks/rules/msw-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: MSW Handler
category: Testing
layer: test-lib/handlers/
composedWith: fixture
---

## MSW Handler

One request handler per API endpoint. Returns a sensible default response when invoked without arguments; accepts an optional resolver for per-test overrides (error responses, custom payloads, assertions on the request). Shared across unit tests and Storybook stories.

### Constraints

- One file per endpoint. Filename mirrors HTTP verb + resource + action: `put-add-to-cart-handler.ts`, `get-products-handler.ts`.
- Export a **single named factory function** — not multiple outcome-specific exports. Per-scenario variation is expressed by passing a resolver at the call site.
- URL uses the shared `host` constant from `@/lib/http` and, for endpoints with query params, the shared `buildUrl` helper — never hand-concatenate URLs.
- Default response body is the minimum valid shape. For collection endpoints, use fixtures with `generateUuid()` IDs — do not inline literal arrays.

### Example — PUT handler

```ts
import { http, HttpResponse } from "msw";

import { host } from "@/lib/http";

import type { PutResolver } from "./resolvers";

export const putAddToCartHandler = (resolver?: PutResolver) =>
http.put(`${host}/carts/:cartId`, (req) => {
if (resolver) return resolver(req);

return HttpResponse.json({});
});
```

### Example — GET handler with query params and fixtures

```ts
import { http, HttpResponse } from "msw";

import { buildUrl } from "@/lib/build-url";
import { host } from "@/lib/http";
import { ProductFixture } from "@/test-lib/fixtures/product-fixture";
import { generateUuid } from "@/test-lib/generate-uuid";

import type { GetResolver } from "./resolvers";

export const getProductsHandler = (resolver?: GetResolver) =>
http.get(
`${host}/${buildUrl("products", { limit: 10, sort: "asc" })}`,
(req) => {
if (resolver) return resolver(req);

return HttpResponse.json({
products: ProductFixture.createCollection([
{ id: generateUuid() },
{ id: generateUuid() },
]),
meta: { limit: 10, sort: "asc", total: 2 },
});
}
);
```

### References

- `rules/fixture.md` — deterministic response payloads
- `rules/component-story-test.md` — handler usage in stories, including per-scenario resolver overrides
Loading
Loading