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
2 changes: 1 addition & 1 deletion skills/react-vite-structure/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Use this skill to organize React + Vite + TypeScript apps around feature modules
- Read `references/ui-error-states.md` for UI loading, empty, query error, partial-data, retry, mutation pending, and mutation error rendering policy.
- Read `references/typescript-and-naming.md` for `tsconfig.json`, `vite.config.ts` path aliases, naming conventions, component/hook/service/type examples, and common shared types.
- Read `references/feature-workflow.md` when adding or scaffolding a new feature module, including types, service, React Query hooks, components, pages, and public exports.
- Read `references/practices-testing-state-quality.md` for component organization, type safety, custom hooks, error handling, environment variables, unit/integration tests, React Query, Zustand, Context API, ESLint, Prettier, and lint-staged guidance.
- Read `references/practices-testing-state-quality.md` for component organization, type safety, custom hooks, error handling, environment variables, unit/integration tests, MSW HTTP-boundary service adapter tests, React Query, Zustand, Context API, ESLint, Prettier, and lint-staged guidance.
- Read `references/storybook.md` when adding or changing UI components/pages, Storybook setup, visual/a11y/interaction test coverage, Storybook decorators, MSW handlers, or shared Storybook harnesses.
- Read `references/documentation-templates.md` when creating feature documentation, changelogs, API docs, component docs, or troubleshooting guides.
- Read `references/project-checklist-and-migration.md` for new-project setup checklists, additional resources, gradual migration phases, and implementation tips.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,81 @@ When a feature uses injected services:
- Test `shared/errors/*`, `shared/services/api/error-mapping.test.ts`, `platform/tauri/tauri-error.test.ts`, and `core/query/domain-query.test.ts` near their implementations.
- Avoid mocking web/Tauri transport directly in React components when service injection is available.

### Web Service Adapter Tests

Test `platform/services/*/web/*.test.ts` at the HTTP boundary when a web adapter wraps a generated SDK or shared API client. Prefer MSW over mocking generated SDK functions when the test should prove URL building, path/query/body serialization, response validation, and API error mapping.

Keep MSW opt-in unless most tests need HTTP interception. Put shared helpers under `src/test/`, return a file-local server from the setup helper, and use strict unhandled-request behavior:

```typescript
// src/test/web-api-msw.ts
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';

export const WEB_API_BASE_URL = 'http://app.test/api/v1';
export const apiUrl = (path: `/${string}`) => `${WEB_API_BASE_URL}${path}`;

export const setupWebApiMsw = () => {
const server = setupServer();

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

return server;
};
```

If the generated client keeps global configuration, set the test base URL in the helper and restore the original config in `afterAll`.

Prefer handlers that assert transport details instead of only returning static JSON:

```typescript
// src/platform/services/products/web/productService.test.ts
import { describe, expect, it } from 'vitest';
import { http, HttpResponse } from 'msw';

import { apiUrl, setupWebApiMsw } from '@/test/web-api-msw';

import { webProductService } from './productService';

const server = setupWebApiMsw();

const product = {
id: 'notebook',
name: 'Notebook',
updatedAt: '2026-05-15T12:00:00.000Z',
};

describe('webProductService', () => {
it('lists category products with path and sort query params', async () => {
server.use(
http.get(apiUrl('/categories/:categoryId/products'), ({ params, request }) => {
const url = new URL(request.url);

expect(params.categoryId).toBe('stationery');
expect(url.searchParams.get('sortDirection')).toBe('desc');
expect(url.searchParams.get('sortField')).toBe('updated');

return HttpResponse.json([product]);
}),
);

await expect(
webProductService.listCategoryProducts('stationery', {
direction: 'desc',
field: 'updated',
}),
).resolves.toEqual({
ok: true,
value: [product],
});
});
});
```

Cover each public web adapter method with focused success tests. Include representative tests for request bodies, `204`/void responses, API errors, and malformed successful responses when the client performs runtime response validation. Keep UI, hook, and page tests on injected fake services unless the test intentionally exercises the HTTP boundary.

---

## State Management Options
Expand Down
29 changes: 29 additions & 0 deletions ui/src/platform/services/bootstrap/web/bootstrapService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { http, HttpResponse } from 'msw'

import type { BootstrapResult } from '@api-generated/clear-api'
import { apiUrl, setupWebApiMsw } from '@/test/web-api-msw'

import { webBootstrapService } from './bootstrapService'

const server = setupWebApiMsw()

const bootstrapResult = {
runtimeProfile: {
formFactor: 'desktop',
runtime: 'web',
},
} satisfies BootstrapResult

describe('webBootstrapService', () => {
it('bootstraps runtime data through the web API', async () => {
server.use(
http.post(apiUrl('/bootstrap'), () => HttpResponse.json(bootstrapResult)),
)

await expect(webBootstrapService.bootstrap()).resolves.toEqual({
ok: true,
value: bootstrapResult,
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest'
import { http, HttpResponse } from 'msw'

import type { SearchResultGroup, SearchScope } from '@api-generated/clear-api'
import { apiUrl, setupWebApiMsw } from '@/test/web-api-msw'

import { webContentSearchService } from './contentSearchService'

const server = setupWebApiMsw()

const scope = {
kind: 'workspace',
workspaceId: 'independent-study',
} satisfies SearchScope

const searchGroups = [
{
kind: 'folder',
results: [
{
id: 'reading-notes',
kind: 'folder',
locationPath: ['Independent Study'],
title: 'Reading Notes',
updatedAt: '2026-05-15T12:00:00.000Z',
workspaceId: 'independent-study',
},
],
},
{
kind: 'deck',
results: [
{
deckIcon: 'book-open',
id: 'world-history',
kind: 'deck',
locationPath: ['Independent Study', 'Reading Notes'],
title: 'World History',
updatedAt: '2026-05-15T12:00:00.000Z',
workspaceId: 'independent-study',
},
],
},
{
kind: 'note',
results: [
{
deckId: 'world-history',
id: 'industrial-revolution-causes',
kind: 'note',
locationPath: ['Independent Study', 'Reading Notes', 'World History'],
noteKind: 'basic',
title: 'Industrial Revolution Causes',
updatedAt: '2026-05-12T12:00:00.000Z',
workspaceId: 'independent-study',
},
],
},
] satisfies SearchResultGroup[]

describe('webContentSearchService', () => {
it('searches content through the web API and maps all result groups', async () => {
server.use(
http.post(apiUrl('/search'), async ({ request }) => {
expect(await request.json()).toEqual({
query: 'history',
scope,
})

return HttpResponse.json(searchGroups)
}),
)

await expect(
webContentSearchService.search(scope, 'history'),
).resolves.toEqual({
ok: true,
value: searchGroups,
})
})
})
Loading