---
id: 11578
title: Extend unit-test setup.mjs to mock Neo.Main and addon.LocalStorage
state: CLOSED
labels:
  - enhancement
  - developer-experience
  - ai
  - testing
  - core
assignees:
  - neo-gpt
createdAt: '2026-05-18T09:51:24Z'
updatedAt: '2026-05-18T11:54:54Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11578'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-18T11:54:54Z'
---
# Extend unit-test setup.mjs to mock Neo.Main and addon.LocalStorage

## Context

Surfaced from downstream dogfooding: a Neo.mjs client application (client project test-management-tool, dogfooding the in-progress v13 dev branch) tried to write its first Playwright unit test that instantiates the app's root `Neo.container.Viewport` subclass via `Neo.create()`. The class loads and its static config verifies correctly, but `Neo.create()` blows up at construction time inside Neo's framework code — not in the downstream app's class — because `test/playwright/setup.mjs` does not mock two surfaces that any production-shaped controller will hit.

This is the second-and-third recurrence of a pattern we already know: framework test mocks that live ad-hoc in per-spec `beforeEach` (see Related), and never make it into the canonical `setup.mjs` until friction surfaces in another spec. Lifting these into `setup.mjs` now is cheap; the alternative is every downstream consumer rediscovering the same gap.

## The Problem

Empirically reproduced via `npm run test-unit` against a minimal sanity spec:

```
TypeError: Cannot read properties of undefined (reading 'setRoute')
    at ViewportController.initAsync (node_modules/neo.mjs/src/controller/Base.mjs:134:57)
    at node_modules/neo.mjs/src/core/Base.mjs:306:13
```

The failing line in `src/controller/Base.mjs` is:

```js
!Neo.config.hash && defaultHash && Neo.Main.setRoute({value: defaultHash, windowId})
```

After stubbing `Neo.Main`, the next failure cascades into `onConstructed` accessing `Neo.main.addon.LocalStorage.readLocalStorageItem({...})`. Neo's stock `setup.mjs` mocks `Neo.main.addon` with `DragDrop`, `Navigator`, and `ResizeObserver` — but no `LocalStorage`.

**Why Neo's internal suite doesn't trip these:** the framework's own unit tests deliberately instantiate stateless or controller-less components (`Neo.core.Base`, `Progress`, plain `Container` populated with buttons). The gap is invisible from inside the framework but immediate for any downstream app whose root viewport has a real controller — i.e., effectively every non-trivial Neo application.

## The Architectural Reality

- `test/playwright/setup.mjs` exports a `setup(options)` helper that mocks `Neo.config`, `Neo.applyDeltas`, `Neo.main`, `Neo.currentWorker`, `Neo.worker` so unit tests can run in a single Node thread without a real worker bridge.
- `Neo.Main` (capital M) is the main-thread proxy used by `Neo.controller.Base.initAsync` for routing. It is structurally distinct from `Neo.main` (lowercase) which holds addon facades. The two share a naming root but live on separate substrate surfaces — both are runtime-only, neither is currently mocked in unitTestMode.
- `Neo.main.addon.LocalStorage` is the addon facade for the persistent-storage main-thread service. Any controller that persists theme/route/filter state will reference it during `onConstructed` or async setup hooks.
- The existing `setup(options)` shape already accepts an options bag (`neoConfig`, `appConfig`), so extending it with feature-flagged mocks is consistent with the established API.

## The Fix

Extend `test/playwright/setup.mjs` to provide stock mocks for both surfaces, gated on opt-out flags so existing tests that intentionally test routing/storage behavior aren't perturbed:

```js
export function setup(options = {}) {
    const {
        neoConfig    = {},
        appConfig    = {},
        mockMain     = true,   // new
        mockLocalStorage = true // new
    } = options;

    // ... existing setup ...

    if (mockMain) {
        Neo.Main ??= {
            setRoute: () => {}
        };
    }

    if (mockLocalStorage) {
        Neo.main.addon.LocalStorage ??= {
            readLocalStorageItem  : async () => ({value: null}),
            updateLocalStorageItem: async () => {},
            removeLocalStorageItem: async () => {}
        };
    }
}
```

Both mocks use `??=` so a caller that pre-installs richer mocks (e.g., a test that exercises route dispatch end-to-end) wins.

Optional follow-up: factor each mock into a named export (e.g., `mockNeoMain()`, `mockLocalStorage()`) so downstream apps can compose them à la carte without re-invoking the full `setup()` cycle. Not required for this ticket.

## Acceptance Criteria

- [ ] `test/playwright/setup.mjs` provides default stub for `Neo.Main.setRoute`, opt-out via `mockMain: false`.
- [ ] `test/playwright/setup.mjs` provides default stub for `Neo.main.addon.LocalStorage` (read / update / remove), opt-out via `mockLocalStorage: false`.
- [ ] Both mocks use `??=` so caller pre-installed mocks take precedence.
- [ ] A regression spec covers the smoke path: instantiating a `Neo.container.Viewport` subclass with a `Neo.controller.Component` controller that declares a `defaultHash` succeeds via `Neo.create()` and tears down cleanly via `destroy()`.
- [ ] `learn/guides/testing/UnitTesting.md` updated to reference the new flags and the "downstream viewport instantiation" use case.

## Out of Scope

- A full extracted "extended setup helper" for downstream consumers (e.g., a client project-side wrapper). Downstream apps can still build their own helpers on top; this ticket centralizes only the universally-needed mocks.
- Mocking `Neo.Main` beyond `setRoute` (e.g., the full worker-bridge protocol). The opt-out flag lets downstream specs install richer mocks when they need to.
- Refactoring `Neo.controller.Base.initAsync` to be defensive about a missing `Neo.Main`. Routing IS a runtime concern; the test environment should provide the stub, not the runtime code.

## Avoided Traps

- **"Mock everything by default with deeply structured stubs."** Rejected — increases setup-time cost and obscures genuine missing-dep failures. Minimal stubs (no-op functions, `{value: null}` returns) are sufficient for the failure modes observed; deeper behavior is downstream's responsibility.
- **"Make `Neo.Main.setRoute` no-op when `unitTestMode` is true inside `controller/Base.mjs`."** Rejected per Out of Scope §3. Runtime code should not branch on test mode; that bleeds the test-mode boundary into runtime substrate.
- **"Wait until v13 stabilizes to revisit test infra."** Rejected — the friction blocks downstream v13 adopters writing their first viewport spec right now. Cheap fix, immediate ROI.

## Related

- **#9443** (closed) — "Stabilize Playwright Unit Tests by Centralizing Global Mocks". Originally proposed centralizing mocks for `Neo.main`, `Neo.currentWorker`, `Neo.worker`, `Neo.applyDeltas` in `setup.mjs`. Pivoted mid-flight to a different root cause (`trap()` for delayed async imports on destroyed components) and closed without landing the centralized-mock surface this ticket targets. `Neo.Main.setRoute` and `Neo.main.addon.LocalStorage` were never on its list. This ticket explicitly fills that gap.
- **#8101** (closed, v11.17.0) — Earlier per-spec mock of `Neo.main.addon.DragDrop.setDragProxyElement` inside a single test's `beforeEach`. Second datapoint that per-spec ad-hoc mocks repeat across the suite; lifting universally-needed surfaces into `setup.mjs` is the substrate-correct primitive.

## Handoff Retrieval Hint

`Retrieval Hint: "setup.mjs mock Neo.Main setRoute LocalStorage downstream viewport unit test"`

## Provenance

Origin: downstream dogfooding session in the client project test-management-tool (GitLab-hosted client app, dogfooding the Neo.mjs v13 dev branch). No Neo Memory Core session ID — the surfacing context lives outside the canonical swarm.

## Timeline

- 2026-05-18T09:51:26Z @neo-opus-ada added the `enhancement` label
- 2026-05-18T09:51:26Z @neo-opus-ada added the `developer-experience` label
- 2026-05-18T09:51:26Z @neo-opus-ada added the `ai` label
- 2026-05-18T09:51:26Z @neo-opus-ada added the `testing` label
- 2026-05-18T09:51:26Z @neo-opus-ada added the `core` label
- 2026-05-18T10:22:58Z @neo-gpt assigned to @neo-gpt
- 2026-05-18T10:30:20Z @neo-gpt cross-referenced by PR #11579
- 2026-05-18T10:47:41Z @neo-gpt referenced in commit `8fde258` - "feat(test): mock main route and local storage in setup (#11578)"
- 2026-05-18T10:47:41Z @neo-gpt referenced in commit `fac9152` - "fix(test): use Neo namespace helper in setup mocks (#11578)"
- 2026-05-18T11:54:54Z @tobiu closed this issue
- 2026-05-18T11:54:55Z @tobiu referenced in commit `bcd8611` - "feat(test): mock main route and local storage in setup (#11578) (#11579)

* feat(test): mock main route and local storage in setup (#11578)

* fix(test): use Neo namespace helper in setup mocks (#11578)

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"

