A Vue 3 app on Vite+ with TWD (Test While Developing) wired into every part of the toolchain: the dev server, vp lint, vp check, the commit hook, coverage and CI.
Vite+ puts dev, build, lint, format, type checking and package management behind one CLI (vp) and one config file (vite.config.ts). TWD follows the same approach. It adds no config files of its own besides twd.config.json, and it runs through vp.
vp install # pnpm under the hood; also downloads Chrome for twd-cli
vp dev # app + TWD sidebar at http://localhost:5173Open the app and the TWD sidebar is on the left. Run tests from it while you work on the page.
Headless, with the dev server still running:
vp run test:ci # every TWD test
vp exec twd-cli run --test "Todos page" # one suite or test, by name
vp exec twd-cli run --record --test "Todos" # record a video per test (needs ffmpeg 8+)| Vite+ piece | What TWD adds | Where |
|---|---|---|
vp dev |
The sidebar, test discovery and request mocking, through the twd() Vite plugin |
plugins in vite.config.ts |
vp lint / vp check |
The twd-lint rules (missing await, .only, wrong runner imports, rule.request.body), loaded as an Oxlint JS plugin |
lint.jsPlugins + lint.overrides |
vp fmt |
Nothing new. TWD tests are formatted like the rest of src/ |
fmt |
staged (commit hook) |
vp check --fix lints and autofixes TWD tests on every commit |
staged |
vp install |
Chrome for twd-cli, through puppeteer's postinstall |
allowBuilds in pnpm-workspace.yaml |
vp exec / vp run |
Runs twd-cli, twd-js and the coverage scripts |
package.json scripts |
CI (setup-vp) |
A headless run, a job summary, the report artifact, coverage and PR recordings | .github/workflows/ |
All of it lives in vite.config.ts:
export default defineConfig({
staged: {
'*': 'vp check --fix', // pre-commit: fmt + lint (incl. TWD rules) + types
},
lint: {
ignorePatterns: ['public/mock-sw.js'], // generated by `twd-js init`
jsPlugins: [
{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' },
{ name: 'twd', specifier: 'twd-lint' }, // ESLint plugin, running in Oxlint
],
overrides: [
{
files: ['**/*.twd.test.{ts,tsx,js,jsx}'], // TWD rules only touch TWD tests
rules: {
'twd/no-focused-tests': 'error',
'twd/runner-imports': 'error',
'twd/no-missing-await': 'error',
'twd/no-request-body': 'error',
},
},
],
// ...
},
fmt: {
ignorePatterns: ['public/mock-sw.js'],
// ...
},
plugins: lazyPlugins(() => [
vue(),
vueDevTools(),
twd({ testFilePattern: '/**/*.twd.test.ts', open: true, position: 'left' }),
istanbul({
include: 'src/*',
exclude: ['node_modules', '**/*.twd.test.ts', '**/twd-tests/**'],
requireEnv: !process.env.CI, // only instrument under `dev:ci`
extension: ['.ts', '.vue'],
}),
]),
})twd()only runs undervp dev(apply: 'serve'). It injects the sidebar and finds the test files, sosrc/main.tshas no TWD code and production builds contain none.twd-lintis an ESLint plugin, and Oxlint runs it unchanged throughjsPlugins. There's no ESLint install and no second lint command. Mistakes in a TWD test show up invp lint, in the editor and in the commit hook, andvp lint --fixapplies the safe fixes (adding a missingawait, fixing runner imports).istanbul()instruments.tsand.vueonly whenCI=true, so a normalvp devstays fast.
twd.config.json is the runner's only setting:
{ "url": "http://localhost:5173", "coverage": true }/todos is a small CRUD page that shows what twd.mockRequest can do. The API is mocked by TWD's service worker, so there's no backend. Outside the tests the page shows its error state, by design.
The tests in src/twd-tests/todos.twd.test.ts cover:
- Loading state:
delay: 500holds the response so the test can see "Loading todos..." first. - Error and recovery: a
500shows the error. The test then mocks a good response under the same name, clicks Retry and the list loads. - Request body:
await twd.waitForRequest('createTodo')returns the rule, andrule.requestis the parsed JSON the page sent (the title is trimmed). - No call made:
twd.getRequestCount('createTodo')is0when validation blocks an empty title. - Network failure:
status: 0simulates a dropped connection, and the typed input is kept. - No refetch: after a PATCH and a DELETE,
getTodoswas still called only once, so the page updates from the responses it got.
await twd.mockRequest('getTodos', {
method: 'GET',
url: '/api/todos',
response: todoList,
status: 200,
delay: 500,
})
await twd.visit('/todos')
const loading = await screenDom.findByText('Loading todos...')
twd.should(loading, 'be.visible')
await twd.waitForRequest('getTodos')Every store exposes $reset(), called in beforeEach. TWD navigates without reloading the page, so Pinia state would otherwise carry over from one test to the next.
vp run dev:ci # CI=true vp dev → instrumented
vp run test:ci # writes .nyc_output/out.json
vp run collect:coverage:text # or :html / :lcov.github/workflows/twd-tests.ymlruns on pushes and PRs tomain:setup-vp, then the Puppeteer cache, thenvp installvp check- starts
dev:ci vp exec twd-cli run- publishes the job summary and the report artifact
- prints coverage
.github/workflows/twd-record.ymlruns when you add therecordlabel to a PR. It records one video for each TWD test the branch added or changed and comments the link on the PR. Create therecordlabel in the repo first.
Most of these came up while wiring TWD into a pnpm + Vite+ project:
- Use
vp exec, notnpx.devEnginespins pnpm, so npm refuses to run in this project (EBADDEVENGINES). This is also why the workflows run each step by hand instead of using theBRIKEV/twd-clicomposite actions, which callnpx. - Approve puppeteer's build. pnpm blocks dependency install scripts by default.
allowBuilds: { puppeteer: true }inpnpm-workspace.yamlletsvp installdownload Chrome fortwd-cli. - Keep
public/mock-sw.jsout offmtandlint.twd-js initgenerates it minified and rewrites it every time it runs, so formatting it is pointless and linting it produces warnings. - Pass both extensions to nyc.
nyc reportdrops.vuefiles by default, and--extension .vueon its own replaces the defaults and drops.tsinstead. The scripts pass--extension .ts --extension .vue.
| Command | What it does |
|---|---|
vp dev |
Dev server with the TWD sidebar |
vp check |
Format, lint (including TWD rules) and type check |
vp lint --fix |
Lint and apply safe fixes, TWD rules included |
vp run test:ci |
Run every TWD test headlessly (twd-cli run) |
vp run dev:ci |
Dev server with coverage instrumentation |
vp run collect:coverage:text |
Coverage report from the last test:ci |
vp build |
Production build (no TWD code) |
src/
api/todos.ts fetch-based API module
stores/ Pinia setup stores, each with $reset()
views/TodosView.vue the demo page
twd-tests/
todos.twd.test.ts the mockRequest showcase
mocks/todos.ts response fixtures
public/mock-sw.js TWD's mock service worker (generated)
twd.config.json twd-cli config
.claude/twd-patterns.md conventions for the /twd Claude Code agent