This guide shows how to use nut.js with Vitest, a blazing-fast unit test framework powered by Vite.
Installation
bash
npm install --save-dev vitest @nut-tree/nut-js @nut-tree/nl-matcherVitest Configuration
Create vitest.config.ts:
typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// Desktop tests need more time
testTimeout: 30000,
hookTimeout: 10000,
// Run tests sequentially (required for desktop automation)
pool: 'forks',
poolOptions: {
forks: {
singleFork: true,
},
},
// Setup file
setupFiles: ['./vitest.setup.ts'],
},
})Setup File
Create vitest.setup.ts:
typescript
import { beforeAll, afterEach } from 'vitest'
import { keyboard, mouse, screen } from '@nut-tree/nut-js'
beforeAll(() => {
// Configure nut.js
keyboard.config.autoDelayMs = 50
mouse.config.autoDelayMs = 50
mouse.config.mouseSpeed = 1500
})
afterEach(async (ctx) => {
// Screenshot on failure
if (ctx.task.result?.state === 'fail') {
const name = ctx.task.name.replace(/\s+/g, '-')
await screen.capture(`./screenshots/${name}-${Date.now()}.png`)
}
})Example Test Suite
typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import {
keyboard,
mouse,
screen,
clipboard,
Key,
centerOf,
imageResource,
Region,
sleep,
} from '@nut-tree/nut-js'
describe('Text Editor Automation', () => {
beforeAll(async () => {
// Open a text editor
if (process.platform === 'darwin') {
await keyboard.pressKey(Key.LeftSuper, Key.Space)
await keyboard.releaseKey(Key.LeftSuper, Key.Space)
await sleep(300)
await keyboard.type('TextEdit')
await keyboard.pressKey(Key.Enter)
await keyboard.releaseKey(Key.Enter)
await sleep(1000)
}
})
afterAll(async () => {
// Close without saving
await keyboard.pressKey(Key.LeftSuper, Key.Q)
await keyboard.releaseKey(Key.LeftSuper, Key.Q)
await sleep(300)
// Don't save dialog
await keyboard.pressKey(Key.LeftSuper, Key.D)
await keyboard.releaseKey(Key.LeftSuper, Key.D)
})
it('should type text into the editor', async () => {
const testText = 'Hello from Vitest!'
await keyboard.type(testText)
// Verify by selecting all and checking
await keyboard.type(Key.LeftSuper, Key.A)
await sleep(200)
// Copy to clipboard
await keyboard.type(Key.LeftSuper, Key.C)
const clipboardText = await clipboard.paste()
expect(clipboardText).toContain(testText)
})
it('should perform find and replace', async () => {
// Clear and type new text
await keyboard.type(Key.LeftSuper, Key.A)
await keyboard.type('foo bar foo baz foo')
// Open find and replace
await keyboard.type(Key.LeftSuper, Key.LeftAlt, Key.F)
await sleep(500)
// Type search term
await keyboard.type('foo')
await keyboard.type(Key.Tab)
await keyboard.type('replaced')
// Replace all
await keyboard.type(Key.LeftSuper, Key.LeftAlt, Key.G)
await sleep(300)
// Close find dialog
await keyboard.type(Key.Escape)
})
})Custom Matchers for Vitest
Create custom matchers in your setup file:
typescript
import { expect } from 'vitest'
import { screen, imageResource, vitestMatchers } from '@nut-tree/nut-js'
import { useNlMatcher } from '@nut-tree/nl-matcher'
useNlMatcher()
expect.extend(vitestMatchers)Usage:
typescript
it('should display the welcome screen', async () => {
await expect(screen).toShow(imageResource('welcome-dialog.png'))
})Running Tests
bash
# Run all tests
npx vitest run
# Watch mode
npx vitest
# With UI
npx vitest --uiCI Configuration
Example GitHub Actions workflow:
yaml
name: Desktop Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx vitest run
env:
CI: true
- uses: actions/upload-artifact@v4
if: failure()
with:
name: screenshots
path: screenshots/