diff --git a/CLAUDE.md b/CLAUDE.md
index 9f11331..61edd80 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -106,7 +106,9 @@ src/hooks/// → 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//Foo.ts` ↔ `src//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
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index a305cea..94e2f9d 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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`,
diff --git a/tests/mocks/renderer-mock.ts b/src/__mocks__/nitro-renderer.ts
similarity index 100%
rename from tests/mocks/renderer-mock.ts
rename to src/__mocks__/nitro-renderer.ts
diff --git a/tests/dedupeBadges.test.ts b/src/api/avatar/dedupeBadges.test.ts
similarity index 95%
rename from tests/dedupeBadges.test.ts
rename to src/api/avatar/dedupeBadges.test.ts
index 103db9d..33067a1 100644
--- a/tests/dedupeBadges.test.ts
+++ b/src/api/avatar/dedupeBadges.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { dedupeBadges } from '../src/api/avatar/dedupeBadges';
+import { dedupeBadges } from './dedupeBadges';
describe('dedupeBadges', () =>
{
diff --git a/tests/api-utils-extra.test.ts b/src/api/utils/api-utils-extra.test.ts
similarity index 96%
rename from tests/api-utils-extra.test.ts
rename to src/api/utils/api-utils-extra.test.ts
index afd2d77..9fb5205 100644
--- a/tests/api-utils-extra.test.ts
+++ b/src/api/utils/api-utils-extra.test.ts
@@ -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', () =>
{
diff --git a/tests/api-utils.test.ts b/src/api/utils/api-utils.test.ts
similarity index 93%
rename from tests/api-utils.test.ts
rename to src/api/utils/api-utils.test.ts
index 816f019..d200be3 100644
--- a/tests/api-utils.test.ts
+++ b/src/api/utils/api-utils.test.ts
@@ -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', () =>
{
diff --git a/tests/friendly-time.test.ts b/src/api/utils/friendly-time.test.ts
similarity index 96%
rename from tests/friendly-time.test.ts
rename to src/api/utils/friendly-time.test.ts
index 3753278..10e7e30 100644
--- a/tests/friendly-time.test.ts
+++ b/src/api/utils/friendly-time.test.ts
@@ -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;
diff --git a/tests/WidgetErrorBoundary.test.tsx b/src/common/error-boundary/WidgetErrorBoundary.test.tsx
similarity index 94%
rename from tests/WidgetErrorBoundary.test.tsx
rename to src/common/error-boundary/WidgetErrorBoundary.test.tsx
index 3d6b4a9..4e76971 100644
--- a/tests/WidgetErrorBoundary.test.tsx
+++ b/src/common/error-boundary/WidgetErrorBoundary.test.tsx
@@ -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', () =>
diff --git a/tests/navigatorRoomCreatorStore.test.ts b/src/components/navigator/views/navigatorRoomCreatorStore.test.ts
similarity index 94%
rename from tests/navigatorRoomCreatorStore.test.ts
rename to src/components/navigator/views/navigatorRoomCreatorStore.test.ts
index e2747c6..be2079d 100644
--- a/tests/navigatorRoomCreatorStore.test.ts
+++ b/src/components/navigator/views/navigatorRoomCreatorStore.test.ts
@@ -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', () =>
{
diff --git a/tests/WiredCreatorTools.helpers.test.ts b/src/components/wired-tools/WiredCreatorTools.helpers.test.ts
similarity index 98%
rename from tests/WiredCreatorTools.helpers.test.ts
rename to src/components/wired-tools/WiredCreatorTools.helpers.test.ts
index 73ce4ae..8698d6f 100644
--- a/tests/WiredCreatorTools.helpers.test.ts
+++ b/src/components/wired-tools/WiredCreatorTools.helpers.test.ts
@@ -6,7 +6,7 @@ import {
formatMonitorSource,
formatVariableTimestamp,
normalizeMonitorReason
-} from '../src/components/wired-tools/WiredCreatorTools.helpers';
+} from './WiredCreatorTools.helpers';
describe('WiredCreatorTools helpers', () =>
{
diff --git a/tests/wiredCreatorToolsUiStore.test.ts b/src/components/wired-tools/wiredCreatorToolsUiStore.test.ts
similarity index 98%
rename from tests/wiredCreatorToolsUiStore.test.ts
rename to src/components/wired-tools/wiredCreatorToolsUiStore.test.ts
index 9618616..1efccae 100644
--- a/tests/wiredCreatorToolsUiStore.test.ts
+++ b/src/components/wired-tools/wiredCreatorToolsUiStore.test.ts
@@ -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,
diff --git a/tests/useCatalog.filters.test.tsx b/src/hooks/catalog/useCatalog.filters.test.tsx
similarity index 99%
rename from tests/useCatalog.filters.test.tsx
rename to src/hooks/catalog/useCatalog.filters.test.tsx
index 4164e1c..59b6446 100644
--- a/tests/useCatalog.filters.test.tsx
+++ b/src/hooks/catalog/useCatalog.filters.test.tsx
@@ -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', () =>
{
diff --git a/tests/useCatalog.helpers.test.ts b/src/hooks/catalog/useCatalog.helpers.test.ts
similarity index 98%
rename from tests/useCatalog.helpers.test.ts
rename to src/hooks/catalog/useCatalog.helpers.test.ts
index 5a64fba..d3bd696 100644
--- a/tests/useCatalog.helpers.test.ts
+++ b/src/hooks/catalog/useCatalog.helpers.test.ts
@@ -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
diff --git a/tests/catalog-favorites.helpers.test.ts b/src/hooks/catalog/useCatalogFavorites.helpers.test.ts
similarity index 96%
rename from tests/catalog-favorites.helpers.test.ts
rename to src/hooks/catalog/useCatalogFavorites.helpers.test.ts
index db2603e..9f8c319 100644
--- a/tests/catalog-favorites.helpers.test.ts
+++ b/src/hooks/catalog/useCatalogFavorites.helpers.test.ts
@@ -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', () =>
{
diff --git a/tests/avatar-info-reducers.test.ts b/src/hooks/rooms/widgets/avatarInfo.reducers.test.ts
similarity index 97%
rename from tests/avatar-info-reducers.test.ts
rename to src/hooks/rooms/widgets/avatarInfo.reducers.test.ts
index bdaffa0..24411c1 100644
--- a/tests/avatar-info-reducers.test.ts
+++ b/src/hooks/rooms/widgets/avatarInfo.reducers.test.ts
@@ -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
diff --git a/tests/useDoorbellState.test.tsx b/src/hooks/rooms/widgets/useDoorbellState.test.tsx
similarity index 97%
rename from tests/useDoorbellState.test.tsx
rename to src/hooks/rooms/widgets/useDoorbellState.test.tsx
index d04be03..407088d 100644
--- a/tests/useDoorbellState.test.tsx
+++ b/src/hooks/rooms/widgets/useDoorbellState.test.tsx
@@ -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
diff --git a/tests/setup.ts b/src/test-setup.ts
similarity index 100%
rename from tests/setup.ts
rename to src/test-setup.ts
diff --git a/tsconfig.json b/tsconfig.json
index e89ab76..04ef8b6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,7 +30,6 @@
},
"include": [
"src",
- "tests",
"node_modules/@nitrots/nitro-renderer/src/**/*.ts"
]
}
diff --git a/vitest.config.mts b/vitest.config.mts
index 7c217c6..59c4208 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -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')
}
}