Testing and verification
What to run before a change is handed over, how unit tests are written, and how to check a change in the running reference app or in a project that installed Mapo from npm.
The gates
Run them from the repository root. All of them must pass, with no new errors and no new warnings.
| Command | What it checks |
|---|---|
pnpm build | Every package and app builds; the MCP knowledge base is regenerated. |
pnpm lint | ESLint over every package's src/. |
pnpm typecheck | tsc --noEmit for TypeScript packages, vue-tsc --noEmit for form and uikit. |
pnpm test | Vitest in the packages that declare a test script: utils, form, mcp. |
pnpm exec vitest run | Every test file in the repository, including core, store and uikit. |
pnpm docs:build | The VitePress site builds. Broken markup fails it; dead links do not (ignoreDeadLinks). |
pnpm test runs after a build (turbo makes test depend on build), because the MCP tests read the generated knowledge base.
Known warnings
These are present on a clean checkout and are not caused by your change:
pnpm lintreports warnings (no errors) informanduikit, mostly Vue template formatting rules.pnpm buildprints, for the apps:SOURCEMAP_BROKENwarnings from@tailwindcss/vite; a[nuxt-i18n]notice thatmapo-i18n.config.mjsis not found in the app'si18n/folder; andOverriding MapoMediaManagerDialog component, because@mapomodule/uikitregisters that component twice (once by name, once as global).pnpm exec vitest runprints a Vite notice thatvitest.config.tshas "ESM syntax in a file loaded as CommonJS", once for each test project.
Any other warning is new: fix it or explain it in the hand-over.
Known gap
core, store and uikit have tests but no test script, so pnpm test and CI skip them. lint-staged still runs them on commit (vitest related), and pnpm exec vitest run from the root runs them on demand. Run it whenever you touch those packages.
Unit tests
- Runner: Vitest, in a Node environment.
- Location: next to the code, in a
__tests__/folder (src/__tests__/orsrc/runtime/__tests__/). - Names:
<subject>.test.tsor<subject>.spec.ts;<Component>.component.test.tsfor a test that mounts a.vuecomponent (see Component tests). - Config: the root
vitest.config.tsaliases the Nuxt virtual module#importsto a mock inpackages/@mapomodule/form/src/__tests__/__mocks__/nuxt-imports.tsand definesimport.meta.serverastrue. It runs two projects:componentfor the*.component.test.tsfiles,nodefor everything else.form,mcpandutilshave their ownvitest.config.ts.
Run a subset while you work:
pnpm --filter @mapomodule/form test # one package
pnpm exec vitest run packages/@mapomodule/store # one folder, from the root
pnpm --filter @mapomodule/utils exec vitest # watch modeComponent tests
A test that mounts a component and drives it through its props and events catches what a test of a pure function cannot: a prop bound under the wrong name, an event nobody listens to, a watcher that resets the state on the first load. The reference is packages/@mapomodule/uikit/src/__tests__/MapoListTable.component.test.ts.
- Environment. The
componentproject runs in Node but compiles modules for the client (vitest.client-environment.ts). Compiled for SSR, a.vuefile can only be rendered to a string, and SSR stops every watcher after its first run. - Mounting. There is no DOM. The test mounts the component with Vue's
createRendererand nodes that are plain objects, so reactivity, watchers and events run as in the browser. - Nuxt, stores and API. In this project
#appresolves to an empty placeholder,packages/@mapomodule/uikit/src/__tests__/__mocks__/nuxt-app.ts. Mock what the component calls withvi.mock("#app", factory), and the stores anduseCrudthe same way. - Child components. Register a stub for every component the template resolves. Give the real props and events to the stubs whose binding is under test (
UPagination: proppage, eventupdate:page). A stub that accepts anything hides a binding bug. - Timers. Use fake timers (
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] })) and settle withvi.runAllTimersAsync(), so the debounced URL writes run inside the test and none leaks into the next one.
What needs a test
- New logic with a branch, a loop, parsing or data mapping.
- Every bug fix: a test that fails before the fix and passes after it.
- Every public helper in
@mapomodule/utils. - Every new MCP tool, recipe, prompt or resource (see Working on the MCP server).
A change of markup or styling alone needs no unit test; it needs the live check below. How a template binds a child's props and events is behaviour, not markup: it needs a component test.
Live verification in the reference app
apps/example-e2e runs without a real backend: its server/api/** routes are an in-memory mock API. It has one page per scenario (auth, permissions, sidebar, form engine, lists, media, menus, i18n, feedback) and it is the test bed of the MCP server.
Check a change there when it affects rendering, routing, middleware, SSR or the MCP live tools:
pnpm build # or keep `pnpm dev:packages` running in stub mode
pnpm dev:example-e2e # http://localhost:3000Log in with
admin/admin. Theeditorandvieweraccounts (password equal to the username) exercise permissions and roles.Request the pages you changed and check the status code and the rendered HTML:
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/login.A new page returns 404 until the dev server restarts: Nuxt's server-side router keeps the route table it started with.
Check the MCP endpoint, which only exists while the dev server runs:
bashcurl -s -X POST http://localhost:3000/mcp/mapo \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"mapo_doctor","arguments":{}}}'Stop the dev server when you are done.
To typecheck an app as its users would:
cd apps/example-e2e
npx nuxt prepare
npx vue-tsc --noEmit -p .nuxt/tsconfig.app.jsonTesting changes in a consumer app
To try a fix in a real project before it is released, link this workspace into an app that installed mapomodule from npm. Run it from the root of this repository:
pnpm dev:link ../my-projectThe path points at the project root or directly at its Nuxt app (e.g. ../my-project/backoffice), and is resolved from your current directory. A project root works when exactly one directory below it depends on mapomodule. Apps inside this workspace are refused: they already use the local packages.
The command (scripts/dev-link.mjs) finds every mapomodule and @mapomodule/* package installed in the app's node_modules (pnpm, yarn or npm layout) and overlays it with the local sources:
src/becomes a copy of the localsrc/, kept in sync while the command runs;dist/becomes stubs that loadsrc/;- the published
dist/andsrc/move to<package>/.mapo-dev-link/, where--restorefinds them.
Everything happens inside the app's node_modules. The app's package.json, lockfile, nuxt.config and Docker files stay untouched, and the linked code resolves vue, nuxt, pinia and every other dependency from the app's own install.
Why not
link:orpnpm link? A symlinked package resolves its dependencies from this repository'snode_modules. The app would load a second copy ofvue,nuxt,piniaandvue-i18n(broken stores and injections) and native binaries built for the host OS, which fail in a Linux container. Inside a container, the symlink target does not even exist.
Workflow
The steps below use a backoffice that runs nuxt dev in Docker with its directory bind-mounted (backoffice/ mounted on /app, Compose service mapo). Without Docker, restart nuxt dev wherever the steps restart the container.
Start the project once, so that the container installs the backoffice dependencies. The command needs the app's
node_modules.Link from the root of this repository and leave the command running:
bashpnpm dev:link ../my-projectNothing else needs to run here: the command copies
src/as it is, so neitherpnpm buildnorpnpm dev:packagesis needed. The exception is@mapomodule/mcp, whose knowledge base is copied from itsdist/knowledge: if the app installs it, runpnpm --filter @mapomodule/mcp buildfirst.Restart the backoffice once, from the project directory, so that it loads the linked modules:
bashdocker compose restart mapoThe log shows
[mapo:dev-link] Using local sources from ….Edit
packages/*/srchere:- runtime code (components, composables, stores, locales) hot-reloads in the browser;
- module setup code (
src/module.tsand the files it imports) restarts Nuxt on its own.
When you are done, stop the command with
Ctrl+C(the link stays in place), then restore the published packages and restart the backoffice:bashpnpm dev:link ../my-project --restore docker compose restart mapo
The container runs yarn or pnpm install at startup. That keeps the link, because the lockfile has not changed and nothing gets reinstalled. If the app's dependencies are reinstalled anyway, a running pnpm dev:link links them again; restart the backoffice afterwards.
Commands
| Command | Effect |
|---|---|
pnpm dev:link <app>... | Link, then keep syncing until Ctrl+C |
pnpm dev:link <app>... --once | Link without watching |
pnpm dev:link <app>... --status | Show which packages are linked |
pnpm dev:link <app>... --restore | Put the published packages back |
Several apps can be linked at once: pnpm dev:link ../project-a ../project-b.
Things to know
- The app gets the sources of the branch checked out here, not of the release it installed. If that branch lacks fixes that the release branch has, the app shows the difference: link from a branch that contains them.
- The local sources run against the dependencies installed in the app. If a package now needs a dependency that the app lacks, the command prints a warning. Add the dependency to the app temporarily, or update
mapomodulethere. - Packages that the app does not install are skipped. For example, an app on an older
mapomodulehas no@mapomodule/mcp, and the meta-module then skips it. - App code that imports
@mapomodule/*directly, such asmapooverride/components, gets the local sources too. - The linked packages ship TypeScript sources, and Nitro skips TypeScript under
node_modules: the link lets Nitro transpile them, which server code such as the@mapomodule/mcphandlers needs. If the dev server never answers and the log shows a NitroRollupErrorinside a linked package, the app is still running an older link: runpnpm dev:link <app> --onceand restart it. - Some Docker setups don't forward file events into the container. If edits never reach the app, set
CHOKIDAR_USEPOLLING=1in the backoffice service's environment. - A linked app also builds with the local sources, so restore it before running
nuxt buildon that checkout. Production images are not affected: they installnode_modulesfrom the registry.
Browser end-to-end tests
There is no browser E2E suite yet. apps/example-e2e is shaped as its target: one page per scenario, sidebar entries generated from route metadata, a mock API with known users. A visual or interaction check is done by hand at review time; list it in the hand-over with the page, the breakpoint and the colour mode to look at.