mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 06:56:20 +00:00
tests: co-locate every Vitest suite next to its subject under src/
Eliminate the parallel `tests/` tree. Each `*.test.ts` / `*.test.tsx` now sits in the same directory as the module it covers, mirroring its filename (`Foo.ts` ↔ `Foo.test.ts`). The renderer-SDK mock used by component / hook tests moves to `src/__mocks__/nitro-renderer.ts` and the Vitest setup file becomes `src/test-setup.ts` — both still wired through `vitest.config.mts` exactly as before, only the paths changed. All 13 suites + 178/178 cases still pass. The production build is unaffected: rollup only follows imports from `src/index.tsx` and never crosses into `.test.ts` files, so test code is naturally tree-shaken out of the bundle. `yarn build` output is byte-for-byte the same on the user-facing chunks. tsconfig drops the now-redundant `tests` include entry. CLAUDE.md 'Layout convention' replaces the old `tests/` row with three rows documenting the new co-located convention, the `__mocks__/` directory and the `test-setup.ts` entry; ARCHITECTURE.md picks up the same update. The 'DO NOT CHANGE' qualifier on the layout is preserved — this rewrite IS the change, decided deliberately to make tests a first-class part of the source tree rather than a sibling project.
This commit is contained in:
@@ -106,7 +106,9 @@ src/hooks/<area>/<feature?>/ → hooks, FLAT files, no per-feature s
|
||||
src/api/ → cross-cutting helpers (LocalizeText, composers, formatters)
|
||||
src/common/ → reusable UI primitives + error boundary
|
||||
src/state/ → Zustand stores (cross-feature only)
|
||||
tests/ → Vitest suites (mirror filename of subject)
|
||||
src/**/*.test.{ts,tsx} → Vitest suites co-located next to their subject (e.g. `Foo.ts` + `Foo.test.ts`)
|
||||
src/__mocks__/ → hand-written renderer-SDK stub for tests (aliased over `@nitrots/nitro-renderer`)
|
||||
src/test-setup.ts → Vitest setupFiles entry (jest-dom matchers, etc.)
|
||||
```
|
||||
|
||||
When splitting a god-hook the convention is **3 files, all flat in the
|
||||
@@ -261,7 +263,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` |
|
||||
| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) |
|
||||
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) |
|
||||
| Vitest | 178/178 cases — pure helpers + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore`) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `tests/mocks/renderer-mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters |
|
||||
| Vitest | 178/178 cases — pure helpers + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore`) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `src/__mocks__/nitro-renderer.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters. **Tests are co-located** under `src/`, alongside their subject. |
|
||||
| Form Actions | Login / Register / Forgot (LoginView.tsx) |
|
||||
| Cherry-picked from `duckietm` PR #126 | `UserAccountSettingsView` (reset password / email / username under user settings), plus the wear-badge popup `canShowWearButton` gating |
|
||||
|
||||
@@ -270,7 +272,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| Split `useChatWidget` / `useAvatarInfoWidget` | Both state-driven via events with no clean imperative actions to extract — skip-motivated. Already touched today for the InfoStand listener move. |
|
||||
| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` | Their "actions" mutate internal state or are tightly interdependent — skip-motivated. |
|
||||
| Hoist Wired Creator Tools **derived** state to the Zustand slice | UI-only flags are already hoisted (`useWiredCreatorToolsUiStore`). What's left is the event-driven derived state — `selectedFurni` / `selectedUser` / `monitorSnapshot` / `variableHighlightOverlays` — which can only move alongside their listener effects (multi-session refactor). |
|
||||
| Widen the component / hook test coverage | Mock layer is in place (`tests/mocks/renderer-mock.ts`) and the first 2 pilots pass. Good follow-up targets: other `*State` hooks built on event reducers, `LoginView` Form Actions happy/error paths, OfferView with `useNitroQuery`. |
|
||||
| Widen the component / hook test coverage | Mock layer is in place (`src/__mocks__/nitro-renderer.ts`) and the first 2 pilots pass. Good follow-up targets: other `*State` hooks built on event reducers, `LoginView` Form Actions happy/error paths, OfferView with `useNitroQuery`. |
|
||||
|
||||
## Known open logic bugs
|
||||
|
||||
@@ -310,7 +312,8 @@ Fix shapes documented; both are reasonable PRs on their own.
|
||||
|
||||
- Architecture doc: `docs/ARCHITECTURE.md`
|
||||
- Test runner config: `vitest.config.mts` (separate from `vite.config.mjs`)
|
||||
- Test setup: `tests/setup.ts`
|
||||
- Test setup: `src/test-setup.ts`
|
||||
- Test convention: co-located under `src/` next to the subject (`src/<path>/Foo.ts` ↔ `src/<path>/Foo.test.ts`). No separate `tests/` tree.
|
||||
- React Query adapter: `src/api/nitro-query/createNitroQuery.ts`
|
||||
- Zustand factory: `src/state/createNitroStore.ts`
|
||||
- Error boundary: `src/common/error-boundary/WidgetErrorBoundary.tsx`
|
||||
@@ -333,7 +336,7 @@ Fix shapes documented; both are reasonable PRs on their own.
|
||||
`useCatalogUiState` / `useCatalogActions` in
|
||||
`src/hooks/catalog/useCatalog.ts` (all 48 consumers migrated;
|
||||
deprecated `useCatalog` shim removed)
|
||||
- Renderer-SDK mock for Vitest: `tests/mocks/renderer-mock.ts`
|
||||
- Renderer-SDK mock for Vitest: `src/__mocks__/nitro-renderer.ts`
|
||||
(aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`).
|
||||
Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` /
|
||||
`clearMockEventDispatcher` helpers used by hook tests, the
|
||||
|
||||
@@ -528,7 +528,7 @@ Pure helpers in `useCatalog.helpers.ts`:
|
||||
visitors) and passes the resulting `visitorCount` into the helper.
|
||||
|
||||
`useCatalog.ts` now imports these instead of defining them inline
|
||||
(net **−75 LOC**). Test file `tests/useCatalog.helpers.test.ts` covers
|
||||
(net **−75 LOC**). Co-located test file `src/hooks/catalog/useCatalog.helpers.test.ts` covers
|
||||
all six helpers with 34 cases (tree depth + offerId mapping,
|
||||
node lookups including root exclusion, the limit-reached / guild-admin
|
||||
fallback / visitors-in-room paths of the placement helper, and the
|
||||
@@ -538,7 +538,7 @@ empty-map / partial-bucket branches of the offer lookup).
|
||||
- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`
|
||||
configured. Separate `vitest.config.mts` so the runner doesn't drag in
|
||||
the renderer SDK aliases from `vite.config.mjs`.
|
||||
- **163 cases passing** across 12 test files. Pure-module suites:
|
||||
- **178 cases passing** across 13 test files, **co-located under `src/`** next to each subject (no separate `tests/` tree). Pure-module suites:
|
||||
- `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot
|
||||
factory.
|
||||
- `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants
|
||||
@@ -580,7 +580,7 @@ empty-map / partial-bucket branches of the offer lookup).
|
||||
`DOORBELL`, dedup duplicates, remove on `RSDE_ACCEPTED` /
|
||||
`RSDE_REJECTED`, ignore stale events, unsubscribe on unmount.
|
||||
|
||||
- **Renderer-SDK mock at `tests/mocks/renderer-mock.ts`** —
|
||||
- **Renderer-SDK mock at `src/__mocks__/nitro-renderer.ts`** —
|
||||
`vitest.config.mts` aliases `@nitrots/nitro-renderer` over this file
|
||||
so jsdom-hosted tests never load Pixi or the message
|
||||
parser/composer registry. The mock exports:
|
||||
@@ -734,7 +734,7 @@ Remaining order of value/risk for the next contributor:
|
||||
each tab. A slice at `src/components/wired-tools/wiredToolsStore.ts`
|
||||
would make each tab subscribe to the keys it needs.
|
||||
4. **Widen the component/hook Vitest coverage.** The renderer-SDK
|
||||
mock layer is in place (`tests/mocks/renderer-mock.ts`) and the
|
||||
mock layer is in place (`src/__mocks__/nitro-renderer.ts`) and the
|
||||
first two pilots — `WidgetErrorBoundary` and `useDoorbellState` —
|
||||
pass. Good follow-up targets: other `*State` hooks built on event
|
||||
reducers (`useFurniChooserState`, `useUserChooserState`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { dedupeBadges } from '../src/api/avatar/dedupeBadges';
|
||||
import { dedupeBadges } from './dedupeBadges';
|
||||
|
||||
describe('dedupeBadges', () =>
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ColorUtils } from '../src/api/utils/ColorUtils';
|
||||
import { FixedSizeStack } from '../src/api/utils/FixedSizeStack';
|
||||
import { LocalizeFormattedNumber } from '../src/api/utils/LocalizeFormattedNumber';
|
||||
import { ColorUtils } from './ColorUtils';
|
||||
import { FixedSizeStack } from './FixedSizeStack';
|
||||
import { LocalizeFormattedNumber } from './LocalizeFormattedNumber';
|
||||
|
||||
describe('LocalizeFormattedNumber', () =>
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CloneObject } from '../src/api/utils/CloneObject';
|
||||
import { ConvertSeconds } from '../src/api/utils/ConvertSeconds';
|
||||
import { LocalizeShortNumber } from '../src/api/utils/LocalizeShortNumber';
|
||||
import { GetWiredTimeLocale } from '../src/api/wired/GetWiredTimeLocale';
|
||||
import { WiredDateToString } from '../src/api/wired/WiredDateToString';
|
||||
import { getPrefixFontStyle, parsePrefixColors, PRESET_PREFIX_FONTS } from '../src/api/utils/PrefixUtils';
|
||||
import { CloneObject } from './CloneObject';
|
||||
import { ConvertSeconds } from './ConvertSeconds';
|
||||
import { LocalizeShortNumber } from './LocalizeShortNumber';
|
||||
import { GetWiredTimeLocale } from '../wired/GetWiredTimeLocale';
|
||||
import { WiredDateToString } from '../wired/WiredDateToString';
|
||||
import { getPrefixFontStyle, parsePrefixColors, PRESET_PREFIX_FONTS } from './PrefixUtils';
|
||||
|
||||
describe('ConvertSeconds', () =>
|
||||
{
|
||||
@@ -5,12 +5,12 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
* with a deterministic stub. The stub returns `key|amount` so each test
|
||||
* can assert both the bucket FriendlyTime chose AND the value it computed.
|
||||
*/
|
||||
vi.mock('../src/api/utils/LocalizeText', () => ({
|
||||
vi.mock('./LocalizeText', () => ({
|
||||
LocalizeText: (key: string, _params?: string[], replacements?: string[]) =>
|
||||
`${ key }|${ replacements?.[0] ?? '' }`
|
||||
}));
|
||||
|
||||
import { FriendlyTime } from '../src/api/utils/FriendlyTime';
|
||||
import { FriendlyTime } from './FriendlyTime';
|
||||
|
||||
const MINUTE = 60;
|
||||
const HOUR = 60 * MINUTE;
|
||||
+2
-2
@@ -4,10 +4,10 @@ import { NitroLogger } from '@nitrots/nitro-renderer';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { FC } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { WidgetErrorBoundary } from '../src/common/error-boundary/WidgetErrorBoundary';
|
||||
import { WidgetErrorBoundary } from './WidgetErrorBoundary';
|
||||
|
||||
// `import { NitroLogger } from '@nitrots/nitro-renderer'` resolves to
|
||||
// `tests/mocks/renderer-mock.ts` via the alias in vitest.config.mts.
|
||||
// `src/__mocks__/nitro-renderer.ts` via the alias in vitest.config.mts.
|
||||
// The SUT imports the same path, so both reach the same vi.fn instance.
|
||||
|
||||
describe('WidgetErrorBoundary', () =>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useRoomCreatorStore } from '../src/components/navigator/views/navigatorRoomCreatorStore';
|
||||
import { useRoomCreatorStore } from './navigatorRoomCreatorStore';
|
||||
|
||||
describe('useRoomCreatorStore', () =>
|
||||
{
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
formatMonitorSource,
|
||||
formatVariableTimestamp,
|
||||
normalizeMonitorReason
|
||||
} from '../src/components/wired-tools/WiredCreatorTools.helpers';
|
||||
} from './WiredCreatorTools.helpers';
|
||||
|
||||
describe('WiredCreatorTools helpers', () =>
|
||||
{
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useWiredCreatorToolsUiStore } from '../src/components/wired-tools/wiredCreatorToolsUiStore';
|
||||
import { useWiredCreatorToolsUiStore } from './wiredCreatorToolsUiStore';
|
||||
|
||||
const INITIAL = {
|
||||
isVisible: false,
|
||||
@@ -75,7 +75,7 @@ vi.mock('use-between', () => ({
|
||||
|
||||
// Import AFTER the mock is set up. The hooks resolve `useBetween` at
|
||||
// import time via the module graph, so the order matters.
|
||||
import { useCatalogActions, useCatalogData, useCatalogUiState } from '../src/hooks/catalog/useCatalog';
|
||||
import { useCatalogActions, useCatalogData, useCatalogUiState } from './useCatalog';
|
||||
|
||||
describe('useCatalog filter contract', () =>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { BuilderFurniPlaceableStatus } from '../src/api/catalog/BuilderFurniPlaceableStatus';
|
||||
import { CatalogType } from '../src/api/catalog/CatalogType';
|
||||
import { BuilderFurniPlaceableStatus } from '../../api/catalog/BuilderFurniPlaceableStatus';
|
||||
import { CatalogType } from '../../api/catalog/CatalogType';
|
||||
import {
|
||||
buildCatalogNodeTree,
|
||||
findNodeById,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getOfferProductKeys,
|
||||
normalizeCatalogType,
|
||||
resolveBuilderFurniPlaceableStatus
|
||||
} from '../src/hooks/catalog/useCatalog.helpers';
|
||||
} from './useCatalog.helpers';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// normalizeCatalogType
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CatalogType } from '../src/api/catalog/CatalogType';
|
||||
import { getOffersStorageKey, getPagesStorageKey, normalizeCatalogType, parseOffers, parsePages, STORAGE_KEY_OFFERS_BUILDER, STORAGE_KEY_OFFERS_NORMAL, STORAGE_KEY_PAGES_BUILDER, STORAGE_KEY_PAGES_NORMAL } from '../src/hooks/catalog/useCatalogFavorites.helpers';
|
||||
import { CatalogType } from '../../api/catalog/CatalogType';
|
||||
import { getOffersStorageKey, getPagesStorageKey, normalizeCatalogType, parseOffers, parsePages, STORAGE_KEY_OFFERS_BUILDER, STORAGE_KEY_OFFERS_NORMAL, STORAGE_KEY_PAGES_BUILDER, STORAGE_KEY_PAGES_NORMAL } from './useCatalogFavorites.helpers';
|
||||
|
||||
describe('normalizeCatalogType', () =>
|
||||
{
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AvatarInfoUser } from '../src/api/room/widgets/AvatarInfoUser';
|
||||
import type { IAvatarInfo } from '../src/api/room/widgets/IAvatarInfo';
|
||||
import { applyFavouriteGroupUpdate, applyUserBadgesUpdate, applyUserFigureUpdate } from '../src/hooks/rooms/widgets/avatarInfo.reducers';
|
||||
import { AvatarInfoUser } from '../../../api/room/widgets/AvatarInfoUser';
|
||||
import type { IAvatarInfo } from '../../../api/room/widgets/IAvatarInfo';
|
||||
import { applyFavouriteGroupUpdate, applyUserBadgesUpdate, applyUserFigureUpdate } from './avatarInfo.reducers';
|
||||
|
||||
/**
|
||||
* Pure reducers for the InfoStand pilot. They take the inspected
|
||||
@@ -3,8 +3,8 @@
|
||||
import { RoomSessionDoorbellEvent } from '@nitrots/nitro-renderer';
|
||||
import { act, cleanup, renderHook } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useDoorbellState } from '../src/hooks/rooms/widgets/useDoorbellState';
|
||||
import { clearMockEventDispatcher, mockEventDispatcher } from './mocks/renderer-mock';
|
||||
import { useDoorbellState } from './useDoorbellState';
|
||||
import { clearMockEventDispatcher, mockEventDispatcher } from '../../../__mocks__/nitro-renderer';
|
||||
|
||||
// Server push helper — mirrors the renderer wire by emitting the same
|
||||
// constants the SUT listens to. The real constructor takes a session
|
||||
@@ -30,7 +30,6 @@
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"tests",
|
||||
"node_modules/@nitrots/nitro-renderer/src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
+7
-8
@@ -6,23 +6,22 @@ import { resolve } from 'path';
|
||||
* dev/build config wires up the renderer SDK via filesystem aliases that
|
||||
* point at sibling working trees (`../renderer`, `../Nitro_Render_V3`).
|
||||
*
|
||||
* Test files were originally written against pure modules (helpers,
|
||||
* stores) that don't pull in the renderer. We now also support
|
||||
* component-level tests by aliasing `@nitrots/nitro-renderer` to a
|
||||
* hand-written stub at `tests/mocks/renderer-mock.ts` so jsdom doesn't
|
||||
* try to evaluate Pixi + the full message parser/composer registry.
|
||||
* Tests live next to their subject under `src/` (`foo.ts` + `foo.test.ts`).
|
||||
* The renderer SDK is aliased to a hand-written stub at
|
||||
* `src/__mocks__/nitro-renderer.ts` so jsdom doesn't try to evaluate
|
||||
* Pixi + the full message parser/composer registry at import time.
|
||||
*/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: false,
|
||||
include: [ 'tests/**/*.test.ts', 'tests/**/*.test.tsx' ],
|
||||
setupFiles: [ './tests/setup.ts' ],
|
||||
include: [ 'src/**/*.test.ts', 'src/**/*.test.tsx' ],
|
||||
setupFiles: [ './src/test-setup.ts' ],
|
||||
css: false
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@nitrots/nitro-renderer': resolve(__dirname, 'tests/mocks/renderer-mock.ts'),
|
||||
'@nitrots/nitro-renderer': resolve(__dirname, 'src/__mocks__/nitro-renderer.ts'),
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user