@mszr/selenita
v0.2.2
Published
IntelliSense testing for TypeScript — completions, hover, diagnostics, and more via a delightful tagged-template API.
Maintainers
Readme
selenita
The best libraries are not only type-safe; they provide great IntelliSense. But testing IntelliSense is so cumbersome it's often ignored. selenita makes it delightful.
selenita exposes the TypeScript Language Service through a tagged-template API. Write completions, hover, and diagnostics assertions the same way you write any other test — no line/column arithmetic, no magic comment strings, no separate DSL.
Why test IntelliSense?
Type safety is not the whole story. When someone uses your library, TypeScript's language service is an invisible co-author — suggesting properties, flagging mistakes, explaining what each parameter means. That experience is part of your API surface.
A refactor that renames an option, changes the order of union members, or introduces a new required field can break completions silently. No runtime error. No failing test. Just a quietly degraded experience for anyone using your library.
selenita gives that surface the same regression protection you give your runtime behavior. If a completion disappears, an unexpected error appears, or two equivalent APIs start diverging — your tests catch it before your users do.
Highlights
- ✨ Zero arithmetic — cursors are first-class JavaScript values you interpolate directly; no line numbers, no character offsets.
- 🧩 Composable —
snippetcaptures reusable code fragments with their own cursors;.for('alias')scopes them for multi-position use. - 🔬 Parity testing —
group+queryGroupassert that a set of APIs expose identical completions, and surface exactly what diverges when they don't. - 📦 Mode matrix —
forModesruns your tests against source,dist-esm, anddist-cjsin one declaration. - 🏃 Synchronous — the TypeScript Language Service is synchronous. No forced
await, no async test boilerplate. - 🔒 Isolated — each test file gets its own TypeScript program. Standard concurrent mode works with zero configuration.
- 🛡️ Safe by design — invalid positions return empty results, never throw. Errors only come from programming mistakes.
- 🪝 Extensible — plain object results work with any
expect. Vitest matcher addon for the best ergonomics.
Quick start
bun add --dev @mszr/selenita typescript # or npm / pnpmselenita uses globalThis.beforeAll/afterAll for lifecycle registration. Enable Vitest globals in your config:
// vitest.config.ts
export default defineConfig({ test: { globals: true } })import { cursor, defineProject } from '@mszr/selenita'
import { describe, expect, it } from 'vitest'
const project = defineProject({ tsconfig: './tsconfig.json' })
describe('createQuery', () => {
it('suggests option keys', () => {
const { completions } = project.query`
import { createQuery } from './src'
createQuery({ ${cursor} })
`
expect(completions).toContain('table')
expect(completions).toContain('limit')
})
it('flags unknown options as errors', () => {
const { errors } = project.check`
import { createQuery } from './src'
createQuery({ tabel: 'users' })
`
expect(errors.length).toBeGreaterThan(0)
})
})Complete example
import { cursor, defineProject, group, snippet } from '@mszr/selenita'
import { describe, expect, it } from 'vitest'
import '@mszr/selenita/vitest'
const project = defineProject({
tsconfig: './tsconfig.json',
aliases: { '#fixtures/*': './tests/fixtures/*' },
})
const queryApis = group('queryApis', ['db.findMany', 'db.findOne', 'db.aggregate'])
const filterArg = snippet`{ status: 'open', ${cursor('filter')} }`
// ─── option completions ───────────────────────────────────────────
describe('createQuery', () => {
it('suggests all options', () => {
const { completions } = project.query`
import { createQuery } from './src'
createQuery({ ${cursor} })
`
expect(completions).toContainCompletions(['table', 'limit', 'orderBy'])
expect(completions).not.toContainCompletion('tabel')
})
it('table is a required string property', () => {
const result = project.query`
import { createQuery } from './src'
createQuery({ ${cursor} })
`
expect(result.completionItem('table')).toHaveKind('property')
expect(result.completionItem('table')).toHaveType('string')
expect(result.completionItem('table')).not.toBeDeprecated()
})
it('completions narrow as options fill in', () => {
const result = project.query`
import { createQuery } from './src'
createQuery({ ${cursor('empty')} })
createQuery({ table: 'users', ${cursor('partial')} })
`
expect(result.at('empty').completions).toContainCompletion('table')
expect(result.at('partial').completions).not.toContainCompletion('table')
})
})
// ─── API parity ───────────────────────────────────────────────────
describe('query API parity', () => {
it('all query methods expose identical filter completions', () => {
const result = project.queryGroup(queryApis, api => snippet`${api}(${filterArg})`)`
import { db } from '#fixtures/db'
`
expect(result.group.at('filter')).toHaveCompletionParity()
})
})
// ─── dist output ─────────────────────────────────────────────────
describe('dist output', () => {
project.forModes(
{
'source': './src/index.ts',
'dist-esm': { entry: './dist/index.js', dts: './dist/index.d.ts' },
},
(project, mode) => {
it(`[${mode}] completions survive compilation`, () => {
const { completions } = project.query`
import { createQuery } from '${project.entry}'
createQuery({ ${cursor} })
`
expect(completions).toContainCompletion('table')
})
}
)
})Documentation
| | |
| --- | --- |
| Getting started [get raw] | Installation, project setup, connecting to your tsconfig |
| Querying [get raw] | project.query, project.check, all result shapes |
| Snippets [get raw] | Reusable fragments, .for() scoping, composition |
| Groups & parity [get raw] | group, project.queryGroup, hasParity, divergence |
| Project config [get raw] | project.with(), project.forModes(), escape hatches |
| Matchers [get raw] | All custom Vitest matchers with examples |
| | |
