Skip to content

Repository files navigation

@magic/test

declaratively test your ecmascript module files

no transpiling of either your codebase nor the tests.

incredibly fast.

html docs

NPM versionCoverage StatusGreenkeeper badgeKnown Vulnerabilities

getting started

be in a nodejs project.

npm i --save-dev @magic/test
mkdir test

create ./test/yourFileToTest.{js,ts}, the filename is used in the test output, the path should be the same as the file in your src dir, the path is used in the log messages.

// ./test/yourLibToTest.{js,ts}importyourLibToTestfrom'../path/to/your/lib.js'exportdefault[{fn: ()=>true,expect: true,info: 'true is true'},// note that the function will be called automagically. expect: true is optional.{fn: yourLibToTest.returnsTrue,/* expect: true, */info: 'yourLibToTest returns true'},// if you need arguments, call the function. also works with async/await.{fn: yourLibToTest.withArgs('argument1','argument2'),expect: 'string',info: 'yourLibToTest.withArgs returns "string"',},// if you absolutely need to nest your function in a function call{fn: ()=>yourLibToTest.withArgs('argument1','argument2'),expect: true,info: 'nested functions work.',},]
npm run scripts

edit package.json:

{"scripts": {"test": "t -p",// quick test, only failing tests log"coverage": "t",// get full test output and coverage reports}}repeatedforeasycopypasting(withoutcomments):
"scripts": {"test": "t -p","coverage": "t",}

run the tests:

 npm test

run coverage reports and get full test report including from passing tests:

 npm run coverage

This library tests itself, have a look at the tests

Checkout @magic/types and the other @magic libraries for more test examples.

example output, from this repository, lots of worker tests: (passing test files are silent if -p is passed)

### Testing package: @magic/test@0.3.15
Ran 2116 tests in 1.4s. Passed 2116/2116 100%

fastest tests from a private project

### Testing package: @artificialmuseum/engine
Ran 90307 tests in 274.5ms. Passed 90307/90307 100%
Ran 90307 tests in 265.5ms. Passed 90307/90307 100%
Ran 90307 tests in 268.1ms. Passed 90307/90307 100%

benchmarks are a lie.

those numbers lie a bit, the actual time to finish usually is around 1-2 seconds for smaller projects, 5-15 seconds for bigger projects with lots of svelte components, mainly caused by c8 coverage, svelte compilation, worker isolation and node.js runtime start times.

data/fs driven test suite creation:
  • expectations for optimal test messages:
  • src and test directories have the same structure and files.
  • tests one src file per test file.
  • tests one function per suite
  • tests one feature per test
Filesystem or Data driven naming for tests

the following directory structure:

./test/
./suite1.js
./suite2.js

creates the same output as exporting the following from ./test/index.js does:

importsuite1from'./suite1.js'importsuite2from'./suite2.js'exportdefault{
suite1,
suite2,}
Important

if test/index.js exists, no other files will be loaded by the discovery mechanism.

if test/lib/index.js exists, no other files from that subdirectory will be loaded.

single test, literal value, function or promise
exportdefault{fn: true,expect: true,info: 'expect true to be true'}// expect: true is the defaultexportdefault{fn: true,info: 'expect true to be true'}// if fn is a function expect is the returned value of the functionexportdefault{fn: ()=>false,expect: false,info: 'expect true to be true'}// if expect is a function the return value of the test get passed to itexportdefault{fn: false,expect: t=>t===false,info: 'expect true to be true'}// if fn is a promise the resolved value will be returnedexportdefault{fn: newPromise(r=>r(true)),expect: true,info: 'expect true to be true'}// if expect is a promise it will resolve before being compared to the fn return valueexportdefault{fn: true,expect: newPromise(r=>r(true)),info: 'expect true to be true'}// callback functions can be tested easily too:import{promise}from'@magic/test'constfnWithCallback=(err,arg,cb)=>cb(err,arg)exportdefault{fn: promise(fnWithCallback(null,'arg',(e,a)=>a)),expect: 'arg'}
testing types

types can be compared using @magic/types

@magic/types is a full featured and thoroughly tested type library without dependencies.

it is exported from this library for convenience.

import{is}from'@magic/test'exportdefault[{fn: ()=>'string',expect: is.string,info: 'test if a function returns a string'},{fn: ()=>'string',expect: is.length.equal(6),info: 'test length of returned value',},// !!! Testing for deep equality. simple.{fn: ()=>[1,2,3],expect: is.deep.equal([1,2,3]),info: 'deep compare arrays/objects for equality',},{fn: ()=>({key: 1,}),expect: is.deep.different({value: 1}),info: 'deep compare arrays/objects for difference',},]
predicates and values array

combine type checks with value checks in a single expect array. each element is checked independently. all must pass (AND semantics):

import{is}from'@magic/test'exportdefault[{fn: ()=>'wrong',expect: [is.string,'wrong'],info: "is a string AND equals 'wrong'",},{fn: ()=>[1,2,3],expect: [is.array,[1,2,3]],info: 'is an array AND deep equals [1, 2, 3]',},{fn: ()=>({key: 'val'}),expect: [is.object,{key: 'val'}],info: 'is an object AND deep equals',},]

mix predicates and value checks freely:

exportdefault{fn: ()=>42,expect: [is.number,v=>v>0,42],info: 'is number, is greater than 0, equals 42',}

predicate elements are functions — evaluated with the test result. non-predicates are compared with strict equality (primitives) or deep equality (objects/arrays). pure value arrays without functions, like expect: [1, 2, 3], still use deep equality as before.

Caveat:

if you want to test if a function is a function, wrap the function, by default, @magic/test will try to execute the function and use the return value for the expect.

import{is}from'@magic/test'constfnToTest=()=>{}exportdefault{fn: ()=>fnToTest,expect: is.function,info: 'function is a function',}
TypeScript support

@magic/test supports TypeScript test files. You can write tests in .ts files and they will be executed directly without transpilation.

// test/mytest.tsimporttype{Test}from'@magic/test'exportdefault[{fn: ()=>true,expect: true,info: 'TypeScript test works!'}]satisfiesTest[]

This requires Node.js 22.18.0 or later.

multiple tests

multiple tests can be created by exporting an array or object of single test objects.

// exporting an arrayexportdefault[{fn: ()=>true,expect: true,info: 'expect true to be true'},{fn: ()=>false,expect: false,info: 'expect false to be false'},]// or exporting an object with named test arraysexportdefault{multipleTests: [{fn: ()=>true,expect: true,info: 'expect true to be true'},{fn: ()=>false,expect: false,info: 'expect false to be false'},],}
promises
import{promise,is,typeTest}from'@magic/test'exportdefault[// kinda clumsy, but works. until you try handling errors.{fn: newPromise(cb=>setTimeout(()=>cb(true),2000)),expect: true,info: 'handle promises',},// better!{fn: promise(cb=>setTimeout(()=>cb(null,true),200)),expect: true,info: 'handle promises in a nicer way',},{fn: promise(cb=>setTimeout(()=>cb(newError('error')),200)),expect: is.error,info: 'handle promise errors in a nice way',},]satisfiesTest[]
running tests multiple times

Use the runs property to run a test multiple times:

import{is}from'@magic/test'exportdefault[{fn: Math.random(),expect: is.number,runs: 5,info: 'runs the test 5 times and expects all returns to be numbers',},]
callback functions
import{promise,is}from'@magic/test'constfnWithCallback=(err,arg,cb)=>cb(err,arg)exportdefault[{fn: promise(cb=>fnWithCallback(null,true,cb)),expect: true,info: 'handle callback functions as promises',},{fn: promise(cb=>fnWithCallback(newError('oops'),true,cb)),expect: is.error,info: 'handle callback function error as promise',},]
run functions before and/or after individual tests

Since before and after usually setup globals, all tests with before/after properties will be run in a worker to make sure they do not pollute the globals

constafter=()=>{global.testing='Test has finished, cleanup.'}constbefore=()=>{global.testing=false// if a function gets returned,// this function will be executed once the test finished.returnafter}exportdefault[{fn: ()=>{global.testing='changed in test'},// if before returns a function, it will execute after the test.
before,
after,expect: ()=>global.testing==='changed in test',},
run functions before and/or after a suite of tests

Suites that use beforeAll, afterAll, beforeEach or afterEach will run in a worker to make sure we do not pollute globals for other suites.

importtype{TestObject}from'@magic/test'constafterAll=()=>{// Suite has finished, cleanup.'global.testing=undefined}constbeforeAll=()=>{global.testing=false// if a function gets returned,// this function will be executed once the test suite finished.returnafterAll}constbeforeEach=()=>{// this will run before EACH test in the suiteglobal.testing=true}constafterEach=()=>{// this will run after EACH test in the suiteglobal.testing=false}exportdefault{// if beforeAll returns a function, it will execute after the test suite.
beforeAll,// this is optional if beforeall returns a function.// in this example, afterAll will trigger twice.
afterAll,// this will run before each test in the suite
beforeEach,// this will run after each test in the suite
afterEach,tests: [{fn: ()=>{global.testing='changed in test'},expect: ()=>global.testing==='changed in test',},],}satisfiesTestObject

File-based Hooks:

You can also create test/beforeAll.js and test/afterAll.js files that run before/after all tests. If the exported function returns another function, it will be executed after the tests complete.

Note: These files must be placed at the roottest/ directory (not in subdirectories).

// test/beforeAll.jsexportdefault()=>{global.setup=true// optionally return a cleanup functionreturn()=>{global.setup=false}}
// test/afterAll.jsexportdefault()=>{// cleanup after all tests}
test @magic-modules

@magic-modules assume all html tags to be globally defined. to create those globals for your test and check if a @magic-module returns the correct markup, call one of the tags in your test function:

exportdefault[{fn: ()=>i('testing'),expect: ['i','testing'],info: '@magic/test can now test html'},]

@magic/test exports some utility functions that make working with complex test workflows simpler.

deep

Exported from @magic/deep, deep equality and comparison utilities.

import{deep,is}from'@magic/test'exportdefault[{fn: ()=>({a: 1,b: 2}),expect: deep.equal({a: 1,b: 2}),info: 'deep equals comparison',},{fn: ()=>({a: 1}),expect: deep.different({a: 2}),info: 'deep different comparison',},{fn: ()=>({a: {b: 1}}),expect: deep.equal({a: {b: 1}}),info: 'nested deep equality',},]

Available functions:

  • deep.equal(a, b) - deep equality check
  • deep.different(a, b) - deep difference check
  • deep.contains(container, item) - deep inclusion check
  • deep.changes(a, b) - get differences between objects
fs

Exported from @magic/fs, file system utilities.

import{fs}from'@magic/test'exportdefault[{fn: async()=>{constcontent=awaitfs.readFile('./package.json','utf-8')returncontent.includes('name')},expect: true,info: 'read file content',},]

Common methods:

  • fs.readFile(path, encoding) - read file content
  • fs.writeFile(path, data) - write file content
  • fs.exists(path) - check if file exists
  • fs.mkdir(path, options) - create directory
  • fs.rmdir(path) - remove directory
  • fs.stat(path) - get file stats
  • fs.readdir(path) - read directory contents
curry

Currying splits a function's arguments into nested functions. Useful for shimming functions with many arguments.

import{curry}from'@magic/test'constcompare=(a,b)=>a===bconstcurried=curry(compare)constshimmed=curried('shimmed_value')exportdefault{fn: shimmed('shimmed_value'),expect: true,info: 'expect will be called with a and b and a will equal b',}
log

Logging utility for test output. Colors supported automatically.

import{log}from'@magic/test'log.debug('Debug info')log.info('Something happened')log.warn('Heads up')log.error('Something went wrong')log.critical('Game over')

supports multiple arguments, just as console.log:

log.info('Testing',library,'at version',version)
vals

Exports JavaScript type constants for testing against any value. Useful for fuzzing and property-based testing.

import{vals,is}from'@magic/test'exportdefault[{fn: ()=>'test',expect: is.string,info: 'test if value is a string'},{fn: ()=>vals.true,expect: true,info: 'boolean true value'},{fn: ()=>vals.email,expect: is.email,info: 'valid email format'},{fn: ()=>vals.error,expect: is.error,info: 'error instance'},]

Available Constants:

CategoryConstants
Primitivestrue, false, number, num, float, int, string, str
Empty valuesnil, emptystr, emptyobject, emptyarray, undef
Collectionsarray, object, obj
Timedate, time
Errorserror, err
Colorsrgb, rgba, hex3, hex6, hexa4, hexa8
Otherfunc, truthy, falsy, email, regexp
env

Environment detection utilities for conditional test behavior.

Available utilities:

  • isNodeProd - checks if NODE_ENV is set to production
  • isNodeDev - checks if NODE_ENV is set to development
  • isProd - checks if -p flag is passed to the CLI
  • isVerbose - checks if -l flag is passed to the CLI
  • getErrorLength - returns error length limit from MAGIC_TEST_ERROR_LENGTH env var (0 = unlimited)
import{env,isProd,isTest,isDev}from'@magic/test'exportdefault[{fn: env.isNodeProd&&process.env.NODE_ENV==='production',expect: true,info: 'checks if NODE_ENV is production',},{fn: env.isNodeDev&&process.env.NODE_ENV==='development',expect: true,info: 'checks if NODE_ENV is development',},{fn: env.isProd&&process.argv.includes('-p'),expect: true,info: 'checks if -p flag is passed',},{fn: env.isVerbose&&process.argv.includes('-l'),expect: true,info: 'checks if -l or --verbose flag is passed',},{fn: env.getErrorLength,expect: undefined,// default, can be overridden by MAGIC_TEST_ERROR_LENGTHinfo: 'get error length limit',},]
promises

Helper function to wrap nodejs callback functions and promises with ease. Handles the try/catch steps internally and returns a resolved or rejected promise.

import{promise,is}from'@magic/test'exportdefault[{fn: promise(cb=>setTimeout(()=>cb(null,true),200)),expect: true,info: 'handle promises in a nice way',},{fn: promise(cb=>setTimeout(()=>cb(newError('error')),200)),expect: is.error,info: 'handle promise errors in a nice way',},]
http

HTTP utility for making requests in tests. Supports both HTTP and HTTPS.

import{http}from'@magic/test'exportdefault[{fn: http.get('https://api.example.com/data'),expect: {success: true},info: 'fetches data from API',},{fn: http.post('https://api.example.com/users',{name: 'John'}),expect: {id: 1,name: 'John'},info: 'creates a new user',},{fn: http.post('http://localhost:3000/data','raw string'),expect: 'raw string',info: 'posts raw string data',},]

Error Handling:

import{http,is}from'@magic/test'exportdefault[{fn: http.get('https://invalid-domain-that-does-not-exist.com'),expect: is.error,info: 'rejects on network error',},{fn: http.get('https://api.example.com/nonexistent'),expect: res=>res.status===404,info: 'handles 404 responses',},]

Note: The HTTP module automatically handles:

  • Protocol detection (HTTP vs HTTPS)
  • JSON parsing for responses with Content-Type: application/json
  • Raw string returns for non-JSON responses
  • rejectUnauthorized: false for self-signed certificates

HttpOptions:

importtype{HttpOptions}from'@magic/test'
OptionTypeDefaultDescription
timeoutnumber30000Request timeout in milliseconds
rejectUnauthorizedbooleantrueReject self-signed certs (set false for testing with local certs)
maxSizenumber-Maximum response size in bytes
requestOptionsRequestOptions-Additional request options
tryCatch

allows to catch and test functions without bubbling the errors up into the runtime

import{is,tryCatch}from'@magic/test'constthrowing=()=>thrownewError('oops')consthealthy=()=>trueexportdefault[{fn: tryCatch(throwing()),expect: is.error,info: 'function throws an error',},{fn: tryCatch(healthy()),expect: true,info: 'function does not throw',},]
error

export @magic/error which returns errors with optional names.

import{error}from'@magic/test'exportdefault[{fn: tryCatch(error('Message','E_NAME')),expect: e=>e.name==='E_NAME'&&e.message==='Message',info: 'Errors have messages and (optional) names.',},]
version

The version plugin checks your code according to a spec defined by you. This is designed to warn you on changes to your exports. Internally, the version function calls @magic/types and all functions exported from it are valid type strings in version specs.

// test/spec.jsimport{version}from'@magic/test'// import your lib as your codebase requires// import * as lib from '../src/index.js'// import lib from '../src/index.jsconstspec={stringValue: 'string',numberValue: 'number',objectValue: ['obj',{key: 'Willbechecked',},],// Test parent object without checking child propertiesobjectNoChildCheck: ['obj',false],}exportdefaultversion(lib,spec)

Note: Using ['obj', false] in a spec will test that the parent is an object without checking the key/value pairs inside.

mock

Mock and spy utilities for function testing.

import{mock,tryCatch}from'@magic/test'exportdefault[{fn: ()=>{constspy=mock.fn()spy('arg1')returnspy.calls.length===1&&spy.calls[0][0]==='arg1'},expect: true,info: 'mock.fn tracks call arguments',},{fn: ()=>{constspy=mock.fn().mockReturnValue('mocked')returnspy()==='mocked'},expect: true,info: 'mock.fn.mockReturnValue sets return value',},{fn: async()=>{constspy=mock.fn().mockThrow(newError('fail'))constcaught=awaittryCatch(spy)()returncaughtinstanceofError},expect: true,info: 'mock.fn.mockThrow works with tryCatch',},{fn: ()=>{constobj={greet: ()=>'hello'}constspy=mock.spy(obj,'greet',()=>'world')constresult=obj.greet()spy.mockRestore()returnresult==='world'&&obj.greet()==='hello'},expect: true,info: 'mock.spy replaces and restores methods',},]

mock.fn properties:

  • calls - Array of all call arguments
  • returns - Array of all return values
  • errors - Array of all thrown errors (null for non-throwing calls)
  • callCount - Number of times called

mock.fn methods:

  • mockReturnValue(value) - Set return value (chainable)
  • mockThrow(error) - Set error to throw (chainable)
  • getCalls() - Get all call arguments
  • getReturns() - Get all return values
  • getErrors() - Get all thrown errors
mock.log
  • mock.log.log() - Logs if not NODE_ENV=production
  • mock.log.warn() - Logs if not NODE_ENV=production
  • mock.log.error() - Always logs
  • mock.log.time() - Logs timing if not NODE_ENV=production
  • mock.log.timeEnd() - Logs timing end if not NODE_ENV=production
has

Functions for asserting object properties without needing explicit type annotations or stringifying functions.

import{has,is}from'@magic/test'

has.property(key, check) - Check a single property. Accepts either a predicate or a literal value

// With predicate{fn: ()=>handleSuiteHooks({}),expect: has.property('afterAllCleanup',is.fn),info: 'returns cleanup function',}// With literal value (uses is.deep.equal){fn: ()=>getUser(),expect: has.property('age',25),info: 'user age is 25',}

has.properties(spec) - Check multiple properties. Mix predicates and literal values

{fn: ()=>getUser(),expect: has.properties({name: is.string,age: is.num}),info: 'user has required properties',}// All literals{fn: ()=>getUser(),expect: has.properties({name: 'John',age: 25}),info: 'exact user match',}

has.any(spec) - Check at least one property matches. Accepts predicates or literals

{fn: ()=>parseResult(),expect: has.any({error: is.error,data: is.object}),info: 'result has either error or data',}// With literals{fn: ()=>parseResult(),expect: has.any({error: 'not found',data: null}),info: 'result has specific error or null data',}

has.nested(path, predicate) - Check a nested property path

{fn: ()=>getData(),expect: has.nested('user.profile.name',is.string),info: 'deep nested property exists',}

has.string(substring) - Check if value is a string containing substring

{fn: ()=>error.message,expect: has.string('failed to connect'),info: 'error message contains helpful context',}

has.key(keyName) - Check if object has a specific key

{fn: ()=>result,expect: has.key('data'),info: 'result has data key',}

has.keys(keyNames[]) - Check if object has all specified keys

{fn: ()=>user,expect: has.keys(['name','email']),info: 'user has required fields',}

has.includes(item) - Check if array or string contains item (uses deep.equal for arrays)

{fn: ()=>roles,expect: has.includes('admin'),info: 'user has admin role',}

has.oneOf(options[]) - Check if value equals one of the options (uses deep.equal)

{fn: ()=>status,expect: has.oneOf(['pending','done','failed']),info: 'status is a valid value',}

has.matches(regex) - Check if string matches regex pattern

{fn: ()=>phoneNumber,expect: has.matches(/^\d{3}-\d{4}$/),info: 'phone number is valid format',}
DOM Environment

@magic/test automatically initializes a DOM environment when imported, making browser APIs available in Node.js.

Available globals:

  • Core: document, window, self, navigator, location, history
  • DOM types: Node, Element, HTMLElement, SVGElement, Document, DocumentFragment
  • Events: Event, CustomEvent, MouseEvent, KeyboardEvent, InputEvent, TouchEvent, PointerEvent
  • Forms: FormData, File, FileList, Blob
  • Networking: URL, URLSearchParams, XMLHttpRequest, fetch, WebSocket
  • Storage: Storage, sessionStorage, localStorage
  • Observers: MutationObserver, IntersectionObserver, ResizeObserver
  • File APIs: FileReader, AbortController, AbortSignal
  • Streams: ReadableStream, WritableStream, TransformStream
  • Misc: DOMParser, XMLSerializer, TextEncoder, TextDecoder, atob, btoa
  • Timers: setTimeout, setInterval, requestAnimationFrame

DOM Utilities:

import{initDOM,getDocument,getWindow}from'@magic/test'// Get the document and window instancesconstdoc=getDocument()constwin=getWindow()// Manually re-initialize if neededinitDOM()

Canvas/Image Polyfills:

  • new Image() - Parses PNG data URLs to extract dimensions
  • canvas.getContext('2d') - Returns node-canvas context
  • canvas.toDataURL() - Serializes canvas to data URL
Svelte Testing

Svelte support is VERY experimental and will be expanded whenever we write tests for our libraries. it currently tests about 20 libraries with a varying degree of complexity and handles most edge cases we encountered

@magic/test has built-in support for testing Svelte 5 components. Compiles Svelte, mounts them in a DOM, and gives you utilities to interact and assert.

internally uses js-dom to create the dom and html elements.

import{mount,tryCatch}from'@magic/test'constcomponent='./path/to/MyComponent.svelte'exportdefault[{
component,props: {message: 'Hello'},fn: ({ target })=>target.innerHTML.includes('Hello'),expect: true,info: 'renders the message prop',},]

Automatic Test Exports

When testing Svelte 5 components, @magic/test automatically exports $state and $derived variables, making them accessible in tests without requiring manual exports.

Note: This automatic export feature is specific to Svelte 5 only. Svelte 4 components do not have this capability.

<!-- Component.svelte -->
<script>let count =$state(0)let doubled =$derived(count *2)// No export needed!</script>
<buttonclass="inc">+</button>
<span>{doubled}</span>
// Test - works automatically!import{mount}from'@magic/test'exportdefault[{component: './Component.svelte',fn: async({ component })=>component.count,// 0expect: 0,info: 'access $state without manual export',},{component: './Component.svelte',fn: async({ component })=>component.doubled,// 0 (derived)expect: 0,info: 'access $derived without manual export',},]

This works automatically for all $state and $derived runes in your component.

Exported Functions:

FunctionDescription
mount(filePath, options)Mounts a Svelte component and returns the target, component instance, and unmount function
html(target)Returns the innerHTML of a mounted component's target element
text(target)Returns the textContent of a target element
component(instance)Returns the component instance for accessing exported values
props(target)Returns an object of attribute name/value pairs from the target element
click(target, selector?)Clicks an element (optionally filtered by CSS selector)
trigger(target, eventType, options?)Dispatches a custom event on an element
scroll(target, x, y)Scrolls an element to x/y coordinates
createSnippet(parent, snippetFn)Creates a snippet and mounts it to a parent element

Test Properties:

PropertyTypeDescription
componentstringPath to the .svelte file
propsobjectProps to pass to the component
fnfunctionTest function receiving { target, component, unmount }

Example: Accessing Component State

import{mount,html}from'@magic/test'import{tick}from'svelte'constcomponent='./src/lib/svelte/components/Counter.svelte'exportdefault[{
component,fn: async({ target, component })=>{// Access exported state from the componentreturncomponent.count},expect: 0,info: 'initial count is 0',},{
component,fn: async({ target, component })=>{// Click the increment button and check statetarget.querySelector('.increment').click()awaittick()returncomponent.count},expect: 1,info: 'count increments on button click',},]

Example: Testing Error Handling

import{mount,tryCatch}from'@magic/test'constcomponent='./src/lib/svelte/components/MyComponent.svelte'exportdefault[{fn: tryCatch(mount,component,{props: null}),expect: t=>t.message==='Props must be an object, got object',info: 'throws when props is null',},{fn: tryCatch(mount,component,{props: 'invalid'}),expect: t=>t.message==='Props must be an object, got string',info: 'throws when props is a string',},]

SvelteKit Mocks:

Mocks SvelteKit's $app modules:

import{browser,dev,prod,createStaticPage}from'@magic/test'exportdefault[{fn: ()=>browser,// true if in browser environmentexpect: false,info: 'not in browser by default',},{fn: ()=>dev,// true if in dev modeexpect: process.env.NODE_ENV==='development',info: 'dev reflects NODE_ENV',},{fn: ()=>prod,// true if in production modeexpect: false,info: 'not in prod by default',},{fn: createStaticPage,expect: t=>typeoft.html==='string'&&typeoft.render==='function',info: 'createStaticPage returns html string and render function',},]
createStaticPage

compileSvelte:

Compile Svelte component source to a module for testing:

import{compileSvelte}from'@magic/test'exportdefault[{fn: async()=>{constsource=`<button>Click</button>`const{ js, css }=compileSvelte(source,'button.svelte')returnjs.code.includes('button')&&css.code===''},expect: true,info: 'compiles Svelte source to module',},]

ensureSvelte:

Lazy-loads the Svelte package. Throws if Svelte is not installed:

import{ensureSvelte}from'@magic/test'exportdefault[{fn: async()=>{constsvelte=awaitensureSvelte()returnsvelte.version.startsWith('5')},expect: true,info: 'loads svelte package',},]

Test Isolation

@magic/test supports test isolation to prevent tests from affecting each other. Tests in the same suite can share state, but you can isolate them:

exportdefault[// This test runs in isolation from others{fn: ()=>{conststate={counter: 0}state.counter++returnstate.counter},expect: 1,info: 'isolated test with local state',},]

Global Isolation Mode:

By default, tests in the same file share global state. To enable strict isolation where each test gets a fresh environment:

__isolate

// This runs each test in isolation with fresh globalsexportconst__isolate=trueexportdefault[{fn: ()=>(global.test=1),expect: 1},{fn: ()=>global.test===undefined,expect: true,info: 'fresh global state'},]

Programmatic Detection:

You can programmatically check if a suite requires isolation using the suiteNeedsIsolation utility:

import{suiteNeedsIsolation}from'@magic/test'constneedsIsolation=suiteNeedsIsolation(tests)

This is useful for custom runners or when building test tooling.

Usage

js api:

// test/index.jsimport{run}from'@magic/test'consttests={lib: [{fn: ()=>true,expect: true,info: 'Expect true to be true'}],}run(tests)

Programmatic API:

The run function accepts test suites and runs them programmatically:

import{run,is}from'@magic/test'consttests={myLib: [{fn: ()=>true,expect: true,info: 'true is true'},{fn: ()=>'test',expect: is.string,info: 'returns a string'},{fn: ()=>({a: 1}),expect: is.deep.equal({a: 1}),info: 'deep equals'},],}// run returns a promiseawaitrun(tests)

cli

package.json (recommended):

Add the magic/test bin scripts to package.json

{
"scripts": {
"test": "t -p",
"coverage": "t"
},
"devDependencies": {
"@magic/test": "github:magic/test"
}
}

then use the npm run scripts

 npm test
npm run coverage
Globally (not recommended):

you can install this library globally, but the recommendation is to add the dependency and scripts to the package.json file.

this both explains to everyone that your app has this dependencies and keeps your bash free of clutter

 npm i -g @magic/test
// run tests in production mode
t -p
// run tests in verbose mode
t

CLI Flags:

FlagAliasesDescription
-p--production, --prodRun tests without coverage (faster)
-l--verbose, --loudShow detailed output including passing tests
-i--includeFiles to include in coverage
-e--excludeFiles to exclude from coverage
--shardsTotal number of shards (use with --shard-id)
--shard-idShard ID (0-indexed, use with --shards)
--workers-wMax parallel workers (default: auto, env: MAGIC_TEST_WORKERS)
--helpShow help text

Note:--shards and --shard-id must be used together. --shard-id is 0-indexed (0 to N-1).

Common Usage:

# Quick test run (no coverage, fails show errors)
npm test# or: t -p# Full test with coverage report
npm run coverage # or: t# Verbose output (shows passing tests)
t -l
# Test with coverage for specific files
t -i "src/**/*.js"# Use glob patterns for include/exclude
t -i "src/**/*.js" -e "**/*.spec.js"# Run tests with sharding (for parallel CI)
t --shards 4 --shard-id 0
#### Sharding Tests
Run tests in parallel across multiple processes to speed up large test suites:
```bash# Run 4 shards, this is shard 0 (of 0-3)t --shards 4 --shard-id 0# Run shard 1t --shards 4 --shard-id 1# Combine with other flagst -p --shards 4 --shard-id 2

Tests are distributed deterministically using a hash of the test file path, ensuring:

  • Each test always runs in the same shard (consistent across runs)
  • No duplicate test execution across shards
  • Even distribution based on file paths

This hash-based approach guarantees that sharding is reproducible and works well with CI caching.

Add to your package.json for CI/CD:

{
"scripts": {
"test": "t -p",
"test:shard:0": "t -p --shards 4 --shard-id 0",
"test:shard:1": "t -p --shards 4 --shard-id 1",
"test:shard:2": "t -p --shards 4 --shard-id 2",
"test:shard:3": "t -p --shards 4 --shard-id 3"
}
}

Or use a single command to run all shards in parallel:

# Run all 4 shards in parallel and wait for all to complete
npm run test:shard:0 & npm run test:shard:1 & npm run test:shard:2 & npm run test:shard:3 &wait

Exit Codes

@magic/test returns specific exit codes to indicate test results:

Exit CodeMeaning
0All tests passed
1One or more tests failed
# Run tests and check exit code
npm testecho"Exit code: $?"# 0 = success, 1 = failure

Performance Tips

Follow these tips to get the most out of @magic/test:

Use the -p flag for development:

# Fast mode - no coverage, only shows failures and result summary
npm test# or
t -p

Shard large test suites:

# Split tests across multiple processes
t --shards 4 --shard-id 0

Control worker concurrency:

# Use fewer workers (keeps CPU threads free for other tasks)
t -w 2
# Or via environment variable
MAGIC_TEST_WORKERS=2 t -p

By default, @magic/test auto-detects CPU count and spawns max(1, CPUs - 2) workers to keep 2 threads free for the system.

Minimize async overhead:

// Slower: unnecessary asyncexportdefault{fn: async()=>{returntrue},expect: true,}// Faster: sync testexportdefault{fn: ()=>true,expect: true,}

Use local state instead of globals:

// Slower: global state requires isolationexportconst__isolate=true// Faster: local state is naturally isolatedexportdefault[{fn: ()=>{constcounter=0return++counter},expect: 1,},]

Batch related tests:

// Faster: single suite with multiple testsexportdefault[{fn: ()=>add(1,2),expect: 3},{fn: ()=>add(0,0),expect: 0},{fn: ()=>add(-1,1),expect: 0},]

Common Pitfalls

Avoid these common mistakes when writing tests:

1. Forgetting to return in async tests:

// Wrong: promise resolves before test checks resultexportdefault{fn: async()=>{constresult=awaitsomeAsyncFunction()// missing return!},expect: true,}// Correct:exportdefault{fn: async()=>{returnawaitsomeAsyncFunction()},expect: true,}

2. Not wrapping callback functions:

// Wrong: function gets called immediatelyexportdefault{fn: doSomething(),// executes immediately!expect: true,}// Correct: wrap in function to defer executionexportdefault{fn: ()=>doSomething(),expect: true,}// Also correct: let @magic/test execute the functionexportdefault{fn: doSomething,expect: true,}

3. Mutating shared state between tests:

// Wrong: counter persists between testsletcounter=0exportdefault[{fn: ()=>++counter,expect: 1},{fn: ()=>++counter,expect: 1},// fails! counter is now 2]// Correct: use local state or reset in beforeEachletcounter=0constbeforeEach=()=>{counter=0}exportdefault{
beforeEach,tests: [{fn: ()=>++counter,expect: 1},{fn: ()=>++counter,expect: 1},// passes - reset before each],}

4. Not awaiting async operations:

// Wrong: test finishes before promise resolvesexportdefault{fn: ()=>{setTimeout(()=>{// This never gets checked!},100)},expect: true,}// Correct: return the promiseexportdefault{fn: ()=>newPromise(resolve=>{setTimeout(()=>resolve(true),100)}),expect: true,}// Or use the promise helper:import{promise}from'@magic/test'exportdefault{fn: promise(cb=>setTimeout(()=>cb(null,true),100)),expect: true,}

5. Incorrect hook usage:

importtype{Test,TestObject}from'@magic/test'// Wrong: before/after hooks on individual tests, not suitesexportdefault[{fn: ()=>true,beforeAll: ()=>{},// wrong! beforeAll is for suitesafterAll: ()=>{},expect: true,},]satisfiesTest[]// Correct: hooks at suite levelconstbeforeAll=()=>{}constafterAll=()=>{}exportdefault{
beforeAll,
afterAll,tests: [{fn: ()=>true,expect: true},],}satisfiesTestObject
Error Codes

@magic/test uses error codes to help with debugging and programmatic error handling. You can import these constants from @magic/test:

CodeDescription
ERRORS.E_EMPTY_SUITETest suite is not exporting any tests
ERRORS.E_RUN_SUITE_UNKNOWNUnknown error occurred while running a suite
ERRORS.E_TEST_NO_FNTest object is missing the fn property
ERRORS.E_TEST_EXPECTTest expectation failed
ERRORS.E_TEST_BEFOREBefore hook failed
ERRORS.E_TEST_AFTERAfter hook failed
ERRORS.E_TEST_FNTest function threw an error
ERRORS.E_NO_TESTSNo test suites found
ERRORS.E_IMPORTFailed to import a test file
ERRORS.E_MAGIC_TESTGeneral test execution error

createError:

import{createError,ERRORS}from'@magic/test'exportdefault[{fn: ()=>createError(ERRORS.E_TEST_NO_FN,'Missing fn property'),expect: e=>e.code==='E_TEST_NO_FN'&&e.message==='Missing fn property',info: 'createError creates errors with code and message',},]

See CHANGELOG.md for release history.

About

ecmascript module testing library. no transpilation of source or tests needed.

Topics

Resources

Stars

0 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages