Write-tests examples by template
This page shows one complete write-tests challenge for every template that supports the mode. Each example lists the reference solution, the starter test the solver begins with, the author reference tests that validate the challenge, and the mutants with their mutants.json.
Every example was run against the real test runner: the author reference tests pass on the reference solution and catch every mutant, and the starter test passes on the reference solution.
New to write-tests challenges?
Read Write-tests challenges first. It explains the file categories, how mutants work and how validation runs.
The examples reuse the same exercise across similar templates, so you can compare how the same behavior is tested in each framework:
| Templates | Exercise |
|---|---|
| Node.js, Node.js with TypeScript, Python | Small pure functions: sum, chunk, clamp |
| NestJS, Fastify, Hono | An HTTP endpoint with validation |
| Svelte, Vanilla JavaScript, Angular, React, Vue, SolidJS | A counter with a lower bound and a reset button |
| Vanilla TypeScript, React with TypeScript, Vue with TypeScript, SolidJS with TypeScript | A todo list that ignores blank and duplicate items |
Terminal templates
Node.js
Test runner: Jest · Example: Sum: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /sum.js |
| Starter Test Files | /sum.test.js (plus /main.js) |
| Author Reference Tests | /sum.reference.test.js |
| Mutants | negatives-bug, plus-one, type-coercion + mutants.json |
- Tests are plain CommonJS:
requirethe solution module the same waymain.jsdoes. - Name test files
*.test.js. They sit next to the reference solution, sorequire('./sum')loads the real implementation, or the mutant during a mutant run. main.jsstays the Run entry point, so solvers can call the function and log values while they explore it.
Reference solution
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('sum expects two numbers');
}
return a + b;
}
module.exports = { sum };Starter test (what the solver starts from)
const { sum } = require('./sum');
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
// Add tests until every hidden bug is caught.
});Author reference tests
const { sum } = require('./sum');
describe('sum (reference suite)', () => {
it('adds two positive numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('adds negative numbers correctly', () => {
expect(sum(-2, -3)).toBe(-5);
expect(sum(-2, 5)).toBe(3);
});
it('throws a TypeError for non-number input', () => {
expect(() => sum('1', 2)).toThrow(TypeError);
expect(() => sum(1, undefined)).toThrow(TypeError);
});
});Mutant negatives-bug replaces /sum.js:
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('sum expects two numbers');
}
return Math.abs(a) + Math.abs(b);
}
module.exports = { sum };{
"negatives-bug": {
"label": "Negatives handling",
"description": "Negative numbers are summed incorrectly."
},
"plus-one": {
"label": "Off-by-one",
"description": "The result is off by one."
},
"type-coercion": {
"label": "Invalid input handling",
"description": "Non-number inputs are coerced instead of rejected."
}
}Node.js with TypeScript
Test runner: Jest (ts-jest) · Example: Chunk: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /chunk.ts |
| Starter Test Files | /chunk.test.ts (plus /main.ts) |
| Author Reference Tests | /chunk.reference.test.ts |
| Mutants | empty-input, invalid-size, off-by-one + mutants.json |
describe,itandexpectare available as globals. You can also import them from@jest/globalsfor explicit typing.- Name test files
*.test.ts. Type errors fail the test file, so keep mutants type-correct: they must compile against the same imports as the reference solution.
Reference solution
export function chunk<T>(list: T[], size: number): T[][] {
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
const chunks: T[][] = [];
for (let index = 0; index < list.length; index += size) {
chunks.push(list.slice(index, index + size));
}
return chunks;
}Starter test (what the solver starts from)
import { chunk } from './chunk';
describe('chunk', () => {
it('splits a list into evenly sized chunks', () => {
expect(chunk([1, 2, 3, 4], 2)).toEqual([
[1, 2],
[3, 4]
]);
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { chunk } from './chunk';
describe('chunk (reference suite)', () => {
it('keeps a shorter trailing chunk', () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});
it('throws a RangeError for non-positive sizes', () => {
expect(() => chunk([1], 0)).toThrow(RangeError);
expect(() => chunk([1], -2)).toThrow(RangeError);
});
it('returns an empty list for empty input', () => {
expect(chunk([], 3)).toEqual([]);
});
});Mutant empty-input replaces /chunk.ts:
export function chunk<T>(list: T[], size: number): T[][] {
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
if (list.length === 0) return [[]];
const chunks: T[][] = [];
for (let index = 0; index < list.length; index += size) {
chunks.push(list.slice(index, index + size));
}
return chunks;
}{
"empty-input": {
"label": "Empty input",
"description": "Empty lists produce the wrong shape."
},
"invalid-size": {
"label": "Invalid size handling",
"description": "Bad sizes are swallowed instead of rejected."
},
"off-by-one": {
"label": "Off-by-one",
"description": "A boundary in the chunking loop is wrong."
}
}Python
Test runner: pytest · Example: Clamp: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /clamp.py |
| Starter Test Files | /test_clamp.py (plus /main.py) |
| Author Reference Tests | /test_clamp_reference.py |
| Mutants | invalid-range, off-by-one, swapped-bounds + mutants.json |
- Tests import the solution through the
challengepackage:from challenge.clamp import clamp. The Run entry (main.py) imports without the prefix (from clamp import clamp). - Name test files
test_*.py. The@m.describe/@m.itdecorators frompytest.markgive the test tree readable names in the results panel. - Use
pytest.raisesto pin down error behavior, a classic source of mutants (a missing validation that no longer raises).
Reference solution
def clamp(value, low, high):
"""Return value limited to the inclusive range [low, high]."""
if low > high:
raise ValueError('low must not be greater than high')
if value < low:
return low
if value > high:
return high
return valueStarter test (what the solver starts from)
from pytest import mark as m
from challenge.clamp import clamp
@m.describe("clamp")
class TestClamp:
@m.it("Returns the value when it is inside the range")
def test_inside_range(self):
assert clamp(5, 0, 10) == 5
# Add tests until every hidden bug is caught.Author reference tests
import pytest
from pytest import mark as m
from challenge.clamp import clamp
@m.describe("clamp (reference suite)")
class TestClampReference:
@m.it("Returns the value when it is inside the range")
def test_inside_range(self):
assert clamp(5, 0, 10) == 5
@m.it("Clamps values below the lower bound up to low")
def test_below_low(self):
assert clamp(-5, 0, 10) == 0
@m.it("Clamps values above the upper bound down to high")
def test_above_high(self):
assert clamp(15, 0, 10) == 10
@m.it("Raises ValueError when low is greater than high")
def test_invalid_range(self):
with pytest.raises(ValueError):
clamp(5, 10, 0)Mutant invalid-range replaces /clamp.py:
def clamp(value, low, high):
"""Return value limited to the inclusive range [low, high]."""
if value < low:
return low
if value > high:
return high
return value{
"invalid-range": {
"label": "Invalid range handling",
"description": "A lower bound above the upper bound is accepted instead of rejected."
},
"off-by-one": {
"label": "Off-by-one",
"description": "One of the boundaries is clamped to the wrong value."
},
"swapped-bounds": {
"label": "Swapped bounds",
"description": "Out-of-range values are clamped to the wrong end of the range."
}
}Backend templates with API tester
NestJS
Test runner: Vitest + @nestjs/testing · Example: Discount: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /src/main.ts, /src/app.module.ts, /src/discount.controller.ts, /src/discount.service.ts |
| Starter Test Files | /discount.controller.test.ts |
| Author Reference Tests | /discount.reference.test.ts |
| Mutants | inverted-discount, no-rounding, percent-over-100 + mutants.json |
- Start every test file with
import 'reflect-metadata';and build a testing module withTest.createTestingModule, registering every controller and provider the unit needs. - Mutant folders mirror the
src/layout:[Mutants]/percent-over-100/src/discount.service.tsreplacessrc/discount.service.ts. - Keep test files at the root and name them
*.test.ts. The web preview excludes them from the Nest build, so a solver's tests never break the running server.
Reference solution
import { Module } from '@nestjs/common';
import { DiscountController } from './discount.controller';
import { DiscountService } from './discount.service';
@Module({
imports: [],
controllers: [DiscountController],
providers: [DiscountService]
})
export class AppModule {}import { Controller, Get, Query } from '@nestjs/common';
import { DiscountService } from './discount.service';
@Controller('discount')
export class DiscountController {
constructor(private readonly discountService: DiscountService) {}
@Get()
apply(@Query('price') price: string, @Query('percent') percent: string): { total: number } {
return { total: this.discountService.apply(Number(price), Number(percent)) };
}
}import { BadRequestException, Injectable } from '@nestjs/common';
@Injectable()
export class DiscountService {
/**
* Applies a percentage discount to a price and returns the discounted price,
* rounded to cents.
*/
apply(price: number, percent: number): number {
if (!Number.isFinite(price) || price < 0) {
throw new BadRequestException('price must be a non-negative number');
}
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
throw new BadRequestException('percent must be between 0 and 100');
}
const discounted = price * (1 - percent / 100);
return Math.round(discounted * 100) / 100;
}
}Starter test (what the solver starts from)
import 'reflect-metadata';
import { describe, it, expect, beforeEach } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { DiscountController } from './src/discount.controller';
import { DiscountService } from './src/discount.service';
describe('DiscountController', () => {
let controller: DiscountController;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [DiscountController],
providers: [DiscountService]
}).compile();
controller = moduleRef.get<DiscountController>(DiscountController);
});
it('applies a 20% discount to a price of 100', () => {
expect(controller.apply('100', '20')).toEqual({ total: 80 });
});
// Add tests until every hidden bug is caught.
});Author reference tests
import 'reflect-metadata';
import { describe, it, expect, beforeEach } from 'vitest';
import { BadRequestException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { DiscountController } from './src/discount.controller';
import { DiscountService } from './src/discount.service';
describe('DiscountController (reference suite)', () => {
let controller: DiscountController;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [DiscountController],
providers: [DiscountService]
}).compile();
controller = moduleRef.get<DiscountController>(DiscountController);
});
it('applies a 20% discount to a price of 100', () => {
expect(controller.apply('100', '20')).toEqual({ total: 80 });
});
it('keeps the full price for a 0% discount', () => {
expect(controller.apply('100', '0')).toEqual({ total: 100 });
});
it('returns 0 for a 100% discount', () => {
expect(controller.apply('100', '100')).toEqual({ total: 0 });
});
it('rounds the discounted price to cents', () => {
expect(controller.apply('19.99', '15')).toEqual({ total: 16.99 });
});
it('rejects a percent above 100', () => {
expect(() => controller.apply('100', '150')).toThrow(BadRequestException);
});
it('rejects a negative percent', () => {
expect(() => controller.apply('100', '-5')).toThrow(BadRequestException);
});
});Mutant inverted-discount replaces /src/discount.service.ts:
import { BadRequestException, Injectable } from '@nestjs/common';
@Injectable()
export class DiscountService {
/**
* Applies a percentage discount to a price and returns the discounted price,
* rounded to cents.
*/
apply(price: number, percent: number): number {
if (!Number.isFinite(price) || price < 0) {
throw new BadRequestException('price must be a non-negative number');
}
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
throw new BadRequestException('percent must be between 0 and 100');
}
const discounted = price * (percent / 100);
return Math.round(discounted * 100) / 100;
}
}{
"inverted-discount": {
"label": "Inverted discount",
"description": "The discount amount is returned instead of the discounted price."
},
"no-rounding": {
"label": "No rounding",
"description": "The discounted price is not rounded to cents."
},
"percent-over-100": {
"label": "Percent over 100 accepted",
"description": "A percent above 100 is applied instead of rejected."
}
}Fastify
Test runner: Vitest + app.inject() · Example: Cart Total: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /src/main.ts, /src/app.ts |
| Starter Test Files | /cart.test.ts |
| Author Reference Tests | /cart.reference.test.ts |
| Mutants | accepts-empty-cart, ignores-quantity, skips-first-item + mutants.json |
- The reference solution exports
buildApp()fromsrc/app.ts, so tests build the app without binding a port and callapp.inject({ method, url, payload }). - Always
await app.close()inafterEach, otherwise open handles leak between test files. - Status codes are behavior too: assert on
statusCodefor rejected requests, not only on the happy-path body.
Reference solution
import Fastify, { type FastifyInstance } from 'fastify';
export interface CartItem {
price: number;
qty: number;
}
interface CartBody {
items?: CartItem[];
}
/**
* Sums price × qty over the items, rounded to cents.
*/
export function cartTotal(items: CartItem[]): number {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return Math.round(total * 100) / 100;
}
export function buildApp(): FastifyInstance {
const app = Fastify({ logger: false });
app.post<{ Body: CartBody }>('/cart/total', async (request, reply) => {
const items = request.body?.items;
if (!Array.isArray(items) || items.length === 0) {
return reply.code(400).send({ error: 'items must be a non-empty array' });
}
const isValidItem = (item: CartItem): boolean =>
Number.isFinite(item.price) && item.price >= 0 && Number.isInteger(item.qty) && item.qty >= 1;
if (!items.every(isValidItem)) {
return reply.code(400).send({ error: 'each item needs a non-negative price and a positive integer qty' });
}
return { total: cartTotal(items) };
});
return app;
}Starter test (what the solver starts from)
import { describe, it, expect, afterEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { buildApp } from './src/app';
describe('POST /cart/total', () => {
let app: FastifyInstance;
afterEach(async () => {
await app?.close();
});
it('sums two items', async () => {
app = buildApp();
const response = await app.inject({
method: 'POST',
url: '/cart/total',
payload: { items: [{ price: 2.5, qty: 2 }, { price: 1.25, qty: 4 }] }
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ total: 10 });
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { buildApp, type CartItem } from './src/app';
describe('POST /cart/total (reference suite)', () => {
let app: FastifyInstance;
beforeEach(() => {
app = buildApp();
});
afterEach(async () => {
await app.close();
});
const total = (items: unknown): Promise<{ statusCode: number; body: unknown }> =>
app.inject({ method: 'POST', url: '/cart/total', payload: { items } }).then((response) => ({
statusCode: response.statusCode,
body: response.json()
}));
it('sums two items', async () => {
const items: CartItem[] = [
{ price: 2.5, qty: 2 },
{ price: 1.25, qty: 4 }
];
expect(await total(items)).toEqual({ statusCode: 200, body: { total: 10 } });
});
it('counts a single item', async () => {
expect(await total([{ price: 3, qty: 1 }])).toEqual({ statusCode: 200, body: { total: 3 } });
});
it('multiplies price by quantity', async () => {
expect(await total([{ price: 2, qty: 3 }])).toEqual({ statusCode: 200, body: { total: 6 } });
});
it('rounds the total to cents', async () => {
expect(await total([{ price: 0.1, qty: 3 }])).toEqual({ statusCode: 200, body: { total: 0.3 } });
});
it('rejects an empty cart with 400', async () => {
const response = await total([]);
expect(response.statusCode).toBe(400);
});
it('rejects a quantity below 1 with 400', async () => {
const response = await total([{ price: 2, qty: 0 }]);
expect(response.statusCode).toBe(400);
});
});Mutant accepts-empty-cart replaces /src/app.ts:
import Fastify, { type FastifyInstance } from 'fastify';
export interface CartItem {
price: number;
qty: number;
}
interface CartBody {
items?: CartItem[];
}
/**
* Sums price × qty over the items, rounded to cents.
*/
export function cartTotal(items: CartItem[]): number {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return Math.round(total * 100) / 100;
}
export function buildApp(): FastifyInstance {
const app = Fastify({ logger: false });
app.post<{ Body: CartBody }>('/cart/total', async (request, reply) => {
const items = request.body?.items;
if (!Array.isArray(items)) {
return reply.code(400).send({ error: 'items must be a non-empty array' });
}
const isValidItem = (item: CartItem): boolean =>
Number.isFinite(item.price) && item.price >= 0 && Number.isInteger(item.qty) && item.qty >= 1;
if (!items.every(isValidItem)) {
return reply.code(400).send({ error: 'each item needs a non-negative price and a positive integer qty' });
}
return { total: cartTotal(items) };
});
return app;
}{
"accepts-empty-cart": {
"label": "Empty cart accepted",
"description": "An empty cart answers with a total instead of being rejected."
},
"ignores-quantity": {
"label": "Quantity ignored",
"description": "Each item is counted once regardless of its quantity."
},
"skips-first-item": {
"label": "First item skipped",
"description": "The first item of the cart is left out of the total."
}
}Hono
Test runner: Vitest + app.request() · Example: Slugify: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /src/main.ts, /src/app.ts |
| Starter Test Files | /slug.test.ts |
| Author Reference Tests | /slug.reference.test.ts |
| Mutants | accepts-empty, edge-dashes, keeps-case + mutants.json |
- The reference solution exports the
Honoinstance fromsrc/app.ts.app.request(path, init?)returns a standardResponsewithout starting a server. - Read the body with
await response.json()orawait response.text(), and checkresponse.statusfor the requests that must be rejected.
Reference solution
import { Hono } from 'hono';
export const app = new Hono();
/**
* Turns free text into a URL slug: trimmed, lowercased, non-alphanumeric runs
* collapsed into single dashes, no leading or trailing dash.
*/
export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
app.get('/slug', (c) => {
const text = c.req.query('text');
if (!text || text.trim().length === 0) {
return c.json({ error: 'text query parameter is required' }, 400);
}
return c.json({ slug: slugify(text) });
});Starter test (what the solver starts from)
import { describe, it, expect } from 'vitest';
import { app } from './src/app';
describe('GET /slug', () => {
it('turns spaces into dashes', async () => {
const response = await app.request('/slug?text=hello%20world');
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ slug: 'hello-world' });
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { describe, it, expect } from 'vitest';
import { app } from './src/app';
describe('GET /slug (reference suite)', () => {
const slug = async (text: string): Promise<{ status: number; body: unknown }> => {
const response = await app.request(`/slug?text=${encodeURIComponent(text)}`);
return { status: response.status, body: await response.json() };
};
it('turns spaces into dashes', async () => {
expect(await slug('hello world')).toEqual({ status: 200, body: { slug: 'hello-world' } });
});
it('lowercases the text', async () => {
expect(await slug('DojoCode')).toEqual({ status: 200, body: { slug: 'dojocode' } });
});
it('collapses punctuation and strips leading/trailing dashes', async () => {
expect(await slug(' Hello, World! ')).toEqual({ status: 200, body: { slug: 'hello-world' } });
});
it('rejects a missing text parameter with 400', async () => {
const response = await app.request('/slug');
expect(response.status).toBe(400);
});
it('rejects an empty text parameter with 400', async () => {
expect((await slug('')).status).toBe(400);
expect((await slug(' ')).status).toBe(400);
});
});Mutant accepts-empty replaces /src/app.ts:
import { Hono } from 'hono';
export const app = new Hono();
/**
* Turns free text into a URL slug: trimmed, lowercased, non-alphanumeric runs
* collapsed into single dashes, no leading or trailing dash.
*/
export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
app.get('/slug', (c) => {
const text = c.req.query('text');
if (text === undefined) {
return c.json({ error: 'text query parameter is required' }, 400);
}
return c.json({ slug: slugify(text) });
});{
"accepts-empty": {
"label": "Empty text accepted",
"description": "An empty text parameter answers with an empty slug instead of being rejected."
},
"edge-dashes": {
"label": "Edge dashes kept",
"description": "Leading and trailing dashes are not stripped from the slug."
},
"keeps-case": {
"label": "Case kept",
"description": "The slug keeps the original casing instead of being lowercased."
}
}Browser templates
Svelte
Test runner: Jest + @testing-library/svelte · Example: Svelte Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.svelte |
| Starter Test Files | /App.spec.js (plus /index.js) |
| Author Reference Tests | /App.reference.spec.js |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
@testing-library/jest-dommatchers (toBeInTheDocument,toBeDisabled) are registered for you.awaiteveryfireEventcall: Svelte flushes the DOM update asynchronously, and an assertion right after an un-awaited click reads the old state.- Name test files
*.spec.jsand callcleanup()inafterEach.
Reference solution
<script>
let count = 0;
function increment() {
count += 1;
}
function decrement() {
if (count > 0) count -= 1;
}
function reset() {
count = 0;
}
</script>
<main>
<p>Count: {count}</p>
<button on:click={decrement} disabled={count === 0}>Decrement</button>
<button on:click={increment}>Increment</button>
<button on:click={reset}>Reset</button>
</main>Starter test (what the solver starts from)
import { render, screen, fireEvent, cleanup } from "@testing-library/svelte";
import App from "./App.svelte";
afterEach(() => cleanup());
describe("Counter", () => {
test("starts at zero", () => {
render(App);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { render, screen, fireEvent, cleanup } from "@testing-library/svelte";
import App from "./App.svelte";
afterEach(() => cleanup());
const button = (name) => screen.getByRole("button", { name });
describe("Counter (reference suite)", () => {
test("starts at zero", () => {
render(App);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
test("increments by one", async () => {
render(App);
await fireEvent.click(button("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
test("decrements by one", async () => {
render(App);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
test("never goes below zero", async () => {
render(App);
expect(button("Decrement")).toBeDisabled();
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await fireEvent.click(button("Increment"));
expect(button("Decrement")).not.toBeDisabled();
});
test("resets to zero after a single increment", async () => {
render(App);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Reset"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
});Mutant double-step replaces /App.svelte:
<script>
let count = 0;
function increment() {
count += 2;
}
function decrement() {
if (count > 0) count -= 1;
}
function reset() {
count = 0;
}
</script>
<main>
<p>Count: {count}</p>
<button on:click={decrement} disabled={count === 0}>Decrement</button>
<button on:click={increment}>Increment</button>
<button on:click={reset}>Reset</button>
</main>{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vanilla JavaScript
Test runner: Jest + jsdom · Example: Vanilla JS Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /counter.js |
| Starter Test Files | /counter.spec.js (plus /index.js, /index.html) |
| Author Reference Tests | /counter.reference.spec.js |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
- There is no Testing Library in this image: query the DOM with
document.querySelectorand trigger events with.click(). - The test setup exposes your
index.htmlasglobalThis.htmlContent. Reset the document inbeforeEach(document.documentElement.innerHTML = globalThis.htmlContent) so every test starts clean, then mount into#app. - Name test files
*.spec.js.
Reference solution
export function createCounter(root) {
let count = 0;
const display = document.createElement("p");
const decrementButton = document.createElement("button");
const incrementButton = document.createElement("button");
const resetButton = document.createElement("button");
decrementButton.textContent = "Decrement";
incrementButton.textContent = "Increment";
resetButton.textContent = "Reset";
function render() {
display.textContent = `Count: ${count}`;
decrementButton.disabled = count === 0;
}
incrementButton.addEventListener("click", () => {
count += 1;
render();
});
decrementButton.addEventListener("click", () => {
if (count > 0) count -= 1;
render();
});
resetButton.addEventListener("click", () => {
count = 0;
render();
});
root.append(display, decrementButton, incrementButton, resetButton);
render();
}Starter test (what the solver starts from)
import { createCounter } from './counter';
describe('counter.js', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createCounter(document.getElementById('app'));
});
test('starts at zero', () => {
expect(document.querySelector('p').textContent).toEqual('Count: 0');
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { createCounter } from './counter';
const button = (label) => [...document.querySelectorAll('button')].find((node) => node.textContent === label);
const count = () => document.querySelector('p').textContent;
describe('counter.js (reference suite)', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createCounter(document.getElementById('app'));
});
test('starts at zero', () => {
expect(count()).toEqual('Count: 0');
});
test('increments by one', () => {
button('Increment').click();
expect(count()).toEqual('Count: 1');
});
test('decrements by one', () => {
button('Increment').click();
button('Increment').click();
button('Decrement').click();
expect(count()).toEqual('Count: 1');
});
test('never goes below zero', () => {
expect(button('Decrement').disabled).toBe(true);
button('Decrement').click();
expect(count()).toEqual('Count: 0');
button('Increment').click();
expect(button('Decrement').disabled).toBe(false);
});
test('resets to zero after a single increment', () => {
button('Increment').click();
button('Reset').click();
expect(count()).toEqual('Count: 0');
});
});Mutant double-step replaces /counter.js:
export function createCounter(root) {
let count = 0;
const display = document.createElement("p");
const decrementButton = document.createElement("button");
const incrementButton = document.createElement("button");
const resetButton = document.createElement("button");
decrementButton.textContent = "Decrement";
incrementButton.textContent = "Increment";
resetButton.textContent = "Reset";
function render() {
display.textContent = `Count: ${count}`;
decrementButton.disabled = count === 0;
}
incrementButton.addEventListener("click", () => {
count += 2;
render();
});
decrementButton.addEventListener("click", () => {
if (count > 0) count -= 1;
render();
});
resetButton.addEventListener("click", () => {
count = 0;
render();
});
root.append(display, decrementButton, incrementButton, resetButton);
render();
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vanilla TypeScript
Test runner: Jest (ts-jest) + jsdom · Example: Vanilla TS Todo List: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /todoList.ts |
| Starter Test Files | /todoList.spec.ts (plus /index.ts, /index.html) |
| Author Reference Tests | /todoList.reference.spec.ts |
| Mutants | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Same DOM setup as Vanilla JavaScript: reset the document from
globalThis.htmlContentinbeforeEachand mount into#app. - The TypeScript
libdoes not includeDOM.Iterable, so spreading aNodeList([...document.querySelectorAll('li')]) does not compile. UseArray.from(...)instead. - Import the Jest globals from
@jest/globalsand name test files*.spec.ts.
Reference solution
export function createTodoList(root: HTMLElement): void {
const items: string[] = [];
const heading = document.createElement("h1");
const input = document.createElement("input");
const addButton = document.createElement("button");
const list = document.createElement("ul");
input.placeholder = "What needs to be done?";
addButton.textContent = "Add";
function render(): void {
heading.textContent = `Todos (${items.length})`;
list.replaceChildren(
...items.map((item) => {
const li = document.createElement("li");
li.textContent = item;
return li;
})
);
}
addButton.addEventListener("click", () => {
const text = input.value.trim();
if (text.length === 0) return;
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.push(text);
input.value = "";
render();
});
root.append(heading, input, addButton, list);
render();
}Starter test (what the solver starts from)
import { createTodoList } from './todoList';
import { describe, test, expect, beforeEach } from '@jest/globals';
describe('todoList.ts', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createTodoList(document.getElementById('app') as HTMLElement);
});
test('starts empty', () => {
expect(document.querySelector('h1').textContent).toEqual('Todos (0)');
expect(document.querySelectorAll('li')).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { createTodoList } from './todoList';
import { describe, test, expect, beforeEach } from '@jest/globals';
const input = (): HTMLInputElement => document.querySelector('input');
const heading = (): string => document.querySelector('h1').textContent;
const items = (): string[] => Array.from(document.querySelectorAll('li')).map((node) => node.textContent);
function add(text: string): void {
input().value = text;
document.querySelector('button').click();
}
describe('todoList.ts (reference suite)', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createTodoList(document.getElementById('app') as HTMLElement);
});
test('starts empty', () => {
expect(heading()).toEqual('Todos (0)');
expect(items()).toEqual([]);
});
test('adds a trimmed todo and counts it', () => {
add(' Buy milk ');
expect(items()).toEqual(['Buy milk']);
expect(heading()).toEqual('Todos (1)');
});
test('clears the input after adding', () => {
add('Buy milk');
expect(input().value).toEqual('');
});
test('ignores blank todos', () => {
add('');
add(' ');
expect(items()).toEqual([]);
expect(heading()).toEqual('Todos (0)');
});
test('ignores duplicates regardless of letter case', () => {
add('Buy milk');
add('buy MILK');
expect(items()).toEqual(['Buy milk']);
expect(heading()).toEqual('Todos (1)');
});
});Mutant accepts-blank replaces /todoList.ts:
export function createTodoList(root: HTMLElement): void {
const items: string[] = [];
const heading = document.createElement("h1");
const input = document.createElement("input");
const addButton = document.createElement("button");
const list = document.createElement("ul");
input.placeholder = "What needs to be done?";
addButton.textContent = "Add";
function render(): void {
heading.textContent = `Todos (${items.length})`;
list.replaceChildren(
...items.map((item) => {
const li = document.createElement("li");
li.textContent = item;
return li;
})
);
}
addButton.addEventListener("click", () => {
const text = input.value.trim();
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.push(text);
input.value = "";
render();
});
root.append(heading, input, addButton, list);
render();
}{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Angular
Test runner: Jest (jest-preset-angular) + @testing-library/angular · Example: Angular Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /app.component.ts, /app.component.html |
| Starter Test Files | /app.component.spec.ts (plus /angular.json, /app.module.ts, /index.html, /index.ts, /style.css) |
| Author Reference Tests | /app.component.reference.spec.ts |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
- This image has no
jest-dommatchers. Assert with plain Jest instead:expect(screen.getByText('Count: 0')).toBeTruthy()andexpect(button.disabled).toBe(true). - The component template (
app.component.html) is part of the reference solution. A mutant can replace either the.tsor the.htmlfile. - Name test files
*.spec.ts. The Angular build that runs the web preview excludes*.spec.ts, so any other name would break the preview.
Reference solution
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: []
})
export class AppComponent {
count = 0;
get canDecrement(): boolean {
return this.count > 0;
}
increment(): void {
this.count += 1;
}
decrement(): void {
if (this.count > 0) this.count -= 1;
}
reset(): void {
this.count = 0;
}
}<div>
<p>Count: {{ count }}</p>
<button (click)="decrement()" [disabled]="!canDecrement">Decrement</button>
<button (click)="increment()">Increment</button>
<button (click)="reset()">Reset</button>
</div>Starter test (what the solver starts from)
import { render, screen, fireEvent } from '@testing-library/angular';
import { AppComponent } from './app.component';
describe('Counter', () => {
it('starts at zero', async () => {
await render(AppComponent);
expect(screen.getByText('Count: 0')).toBeTruthy();
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { render, screen, fireEvent } from '@testing-library/angular';
import { AppComponent } from './app.component';
const button = (name: string): HTMLButtonElement => screen.getByRole('button', { name }) as HTMLButtonElement;
describe('Counter (reference suite)', () => {
it('starts at zero', async () => {
await render(AppComponent);
expect(screen.getByText('Count: 0')).toBeTruthy();
});
it('increments by one', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});
it('decrements by one', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});
it('never goes below zero', async () => {
await render(AppComponent);
expect(button('Decrement').disabled).toBe(true);
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeTruthy();
fireEvent.click(button('Increment'));
expect(button('Decrement').disabled).toBe(false);
});
it('resets to zero after a single increment', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeTruthy();
});
});Mutant double-step replaces /app.component.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: []
})
export class AppComponent {
count = 0;
get canDecrement(): boolean {
return this.count > 0;
}
increment(): void {
this.count += 2;
}
decrement(): void {
if (this.count > 0) this.count -= 1;
}
reset(): void {
this.count = 0;
}
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}React
Test runner: Jest + @testing-library/react · Example: React Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.jsx |
| Starter Test Files | /App.spec.js (plus /index.jsx) |
| Author Reference Tests | /App.reference.spec.js |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
- Use JSX in
*.spec.jsfiles: the image compiles them with Babel's React preset. - Query by role (
getByRole('button', { name: 'Increment' })) so tests follow what users see, thenfireEvent.clickand assert on the new text. - Call
cleanupinafterEachso components from previous tests do not stay mounted.
Reference solution
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const increment = () => setCount((current) => current + 1);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement} disabled={count === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default App;Starter test (what the solver starts from)
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import App from './App.jsx';
afterEach(cleanup);
describe('Counter', () => {
test('starts at zero', () => {
render(<App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import App from './App.jsx';
afterEach(cleanup);
const button = (name) => screen.getByRole('button', { name });
describe('Counter (reference suite)', () => {
test('starts at zero', () => {
render(<App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('increments by one', () => {
render(<App />);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements by one', () => {
render(<App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('never goes below zero', () => {
render(<App />);
expect(button('Decrement')).toBeDisabled();
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(button('Increment'));
expect(button('Decrement')).not.toBeDisabled();
});
test('resets to zero after a single increment', () => {
render(<App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
});Mutant double-step replaces /App.jsx:
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const increment = () => setCount((current) => current + 2);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement} disabled={count === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default App;{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}React with TypeScript
Test runner: Jest (ts-jest) + @testing-library/react · Example: React TS Todo List: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.tsx |
| Starter Test Files | /App.spec.ts (plus /index.tsx) |
| Author Reference Tests | /App.reference.spec.ts |
| Mutants | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Test files are
*.spec.ts, not.tsx: render withReact.createElement(App)instead of JSX. - Import
@testing-library/jest-dom/jest-globalsfirst sotoBeInTheDocument()is typed and registered. - For controlled inputs use
fireEvent.change(input, { target: { value } }).
Reference solution
import React, { useState } from "react";
const App: React.FC = () => {
const [items, setItems] = useState<string[]>([]);
const [draft, setDraft] = useState("");
const addTodo = (): void => {
const text = draft.trim();
if (text.length === 0) return;
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items, text]);
setDraft("");
};
return (
<div>
<h1>Todos ({items.length})</h1>
<input
placeholder="What needs to be done?"
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default App;Starter test (what the solver starts from)
import '@testing-library/jest-dom/jest-globals';
import * as React from "react";
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import App from "./App";
describe("TodoList", () => {
it("starts empty", () => {
render(React.createElement(App));
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Author reference tests
import '@testing-library/jest-dom/jest-globals';
import * as React from "react";
import { describe, expect, it } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react';
import App from "./App";
const input = (): HTMLInputElement => screen.getByPlaceholderText("What needs to be done?") as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole("listitem").map((node) => node.textContent ?? "");
function add(text: string): void {
fireEvent.change(input(), { target: { value: text } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
}
describe("TodoList (reference suite)", () => {
it("starts empty", () => {
render(React.createElement(App));
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(items()).toEqual([]);
});
it("adds a trimmed todo and counts it", () => {
render(React.createElement(App));
add(" Buy milk ");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
it("clears the input after adding", () => {
render(React.createElement(App));
add("Buy milk");
expect(input().value).toBe("");
});
it("ignores blank todos", () => {
render(React.createElement(App));
add("");
add(" ");
expect(items()).toEqual([]);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
});
it("ignores duplicates regardless of letter case", () => {
render(React.createElement(App));
add("Buy milk");
add("buy MILK");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
});Mutant accepts-blank replaces /App.tsx:
import React, { useState } from "react";
const App: React.FC = () => {
const [items, setItems] = useState<string[]>([]);
const [draft, setDraft] = useState("");
const addTodo = (): void => {
const text = draft.trim();
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items, text]);
setDraft("");
};
return (
<div>
<h1>Todos ({items.length})</h1>
<input
placeholder="What needs to be done?"
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default App;{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Vue
Test runner: Jest + @testing-library/vue · Example: Vue Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.vue |
| Starter Test Files | /App.spec.js (plus /index.js) |
| Author Reference Tests | /App.reference.spec.js |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
- Import
@testing-library/jest-dom/jest-globalsand takedescribe,it,expectfrom@jest/globals. awaiteveryfireEventcall: Vue updates the DOM on the next tick.- Name test files
*.spec.js.
Reference solution
<template>
<div>
<p>Count: {{ count }}</p>
<button :disabled="count === 0" @click="decrement">Decrement</button>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value += 1;
}
function decrement() {
if (count.value > 0) count.value -= 1;
}
function reset() {
count.value = 0;
}
</script>Starter test (what the solver starts from)
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import Counter from "./App.vue";
describe('Counter', () => {
it("starts at zero", () => {
render(Counter);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Author reference tests
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import Counter from "./App.vue";
const button = (name) => screen.getByRole("button", { name });
describe('Counter (reference suite)', () => {
it("starts at zero", () => {
render(Counter);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("increments by one", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("decrements by one", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("never goes below zero", async () => {
render(Counter);
expect(button("Decrement")).toBeDisabled();
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await fireEvent.click(button("Increment"));
expect(button("Decrement")).not.toBeDisabled();
});
it("resets to zero after a single increment", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Reset"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
});Mutant double-step replaces /App.vue:
<template>
<div>
<p>Count: {{ count }}</p>
<button :disabled="count === 0" @click="decrement">Decrement</button>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value += 2;
}
function decrement() {
if (count.value > 0) count.value -= 1;
}
function reset() {
count.value = 0;
}
</script>{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vue with TypeScript
Test runner: Jest (ts-jest) + @testing-library/vue · Example: Vue TS Todo List: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.vue |
| Starter Test Files | /App.spec.ts (plus /index.ts) |
| Author Reference Tests | /App.reference.spec.ts |
| Mutants | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Same setup as Vue, with
*.spec.tstest files and<script lang="ts" setup>components. - For
v-modelinputs useawait fireEvent.update(input, value): it sets the value and fires the right event in one call.
Reference solution
<template>
<div>
<h1>Todos ({{ items.length }})</h1>
<input v-model="draft" placeholder="What needs to be done?" />
<button @click="addTodo">Add</button>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const items = ref<string[]>([]);
const draft = ref("");
function addTodo(): void {
const text = draft.value.trim();
if (text.length === 0) return;
if (items.value.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.value.push(text);
draft.value = "";
}
</script>Starter test (what the solver starts from)
import '@testing-library/jest-dom/jest-globals';
import { render } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import TodoList from "./App.vue";
describe('TodoList', () => {
it("starts empty", () => {
render(TodoList);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Author reference tests
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import TodoList from "./App.vue";
const input = (): HTMLInputElement => screen.getByPlaceholderText("What needs to be done?") as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole("listitem").map((node) => node.textContent ?? "");
async function add(text: string): Promise<void> {
await fireEvent.update(input(), text);
await fireEvent.click(screen.getByRole("button", { name: "Add" }));
}
describe('TodoList (reference suite)', () => {
it("starts empty", () => {
render(TodoList);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(items()).toEqual([]);
});
it("adds a trimmed todo and counts it", async () => {
render(TodoList);
await add(" Buy milk ");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
it("clears the input after adding", async () => {
render(TodoList);
await add("Buy milk");
expect(input().value).toBe("");
});
it("ignores blank todos", async () => {
render(TodoList);
await add("");
await add(" ");
expect(items()).toEqual([]);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
});
it("ignores duplicates regardless of letter case", async () => {
render(TodoList);
await add("Buy milk");
await add("buy MILK");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
});Mutant accepts-blank replaces /App.vue:
<template>
<div>
<h1>Todos ({{ items.length }})</h1>
<input v-model="draft" placeholder="What needs to be done?" />
<button @click="addTodo">Add</button>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const items = ref<string[]>([]);
const draft = ref("");
function addTodo(): void {
const text = draft.value.trim();
if (items.value.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.value.push(text);
draft.value = "";
}
</script>{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}SolidJS
Test runner: Vitest + @solidjs/testing-library · Example: Solid Counter: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.jsx |
| Starter Test Files | /App.spec.jsx (plus /main.jsx) |
| Author Reference Tests | /App.reference.spec.jsx |
| Mutants | double-step, negative-allowed, reset-guard + mutants.json |
- Render with a function:
render(() => <App />). Thejest-dommatchers are registered by the test setup. - Solid updates the DOM synchronously, so assertions can follow
fireEvent.clickdirectly. - Name test files
*.spec.jsx.
Reference solution
import { createSignal } from 'solid-js';
export default function App() {
const [count, setCount] = createSignal(0);
const increment = () => setCount((current) => current + 1);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={decrement} disabled={count() === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}Starter test (what the solver starts from)
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
describe('Counter', () => {
test('starts at zero', () => {
render(() => <App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
const button = (name) => screen.getByRole('button', { name });
describe('Counter (reference suite)', () => {
test('starts at zero', () => {
render(() => <App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('increments by one', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements by one', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('never goes below zero', () => {
render(() => <App />);
expect(button('Decrement')).toBeDisabled();
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(button('Increment'));
expect(button('Decrement')).not.toBeDisabled();
});
test('resets to zero after a single increment', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
});Mutant double-step replaces /App.jsx:
import { createSignal } from 'solid-js';
export default function App() {
const [count, setCount] = createSignal(0);
const increment = () => setCount((current) => current + 2);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={decrement} disabled={count() === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}SolidJS with TypeScript
Test runner: Vitest + @solidjs/testing-library · Example: Solid TS Todo List: Write the Tests
| Category | Files |
|---|---|
| Reference Solution | /App.tsx |
| Starter Test Files | /App.spec.tsx (plus /main.tsx) |
| Author Reference Tests | /App.reference.spec.tsx |
| Mutants | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Same setup as SolidJS, with
*.spec.tsxtest files. - For inputs bound with
onInput, usefireEvent.input(input, { target: { value } }).
Reference solution
import { createSignal, For } from 'solid-js';
export default function App() {
const [items, setItems] = createSignal<string[]>([]);
const [draft, setDraft] = createSignal('');
const addTodo = (): void => {
const text = draft().trim();
if (text.length === 0) return;
if (items().some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items(), text]);
setDraft('');
};
return (
<div>
<h1>Todos ({items().length})</h1>
<input
placeholder="What needs to be done?"
value={draft()}
onInput={(event) => setDraft(event.currentTarget.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
<For each={items()}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
);
}Starter test (what the solver starts from)
import { render, screen } from '@solidjs/testing-library';
import App from './App';
describe('TodoList', () => {
test('starts empty', () => {
render(() => <App />);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Author reference tests
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
const input = (): HTMLInputElement => screen.getByPlaceholderText('What needs to be done?') as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole('listitem').map((node) => node.textContent ?? '');
function add(text: string): void {
fireEvent.input(input(), { target: { value: text } });
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
}
describe('TodoList (reference suite)', () => {
test('starts empty', () => {
render(() => <App />);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
expect(items()).toEqual([]);
});
test('adds a trimmed todo and counts it', () => {
render(() => <App />);
add(' Buy milk ');
expect(items()).toEqual(['Buy milk']);
expect(screen.getByText('Todos (1)')).toBeInTheDocument();
});
test('clears the input after adding', () => {
render(() => <App />);
add('Buy milk');
expect(input().value).toBe('');
});
test('ignores blank todos', () => {
render(() => <App />);
add('');
add(' ');
expect(items()).toEqual([]);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
});
test('ignores duplicates regardless of letter case', () => {
render(() => <App />);
add('Buy milk');
add('buy MILK');
expect(items()).toEqual(['Buy milk']);
expect(screen.getByText('Todos (1)')).toBeInTheDocument();
});
});Mutant accepts-blank replaces /App.tsx:
import { createSignal, For } from 'solid-js';
export default function App() {
const [items, setItems] = createSignal<string[]>([]);
const [draft, setDraft] = createSignal('');
const addTodo = (): void => {
const text = draft().trim();
if (items().some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items(), text]);
setDraft('');
};
return (
<div>
<h1>Todos ({items().length})</h1>
<input
placeholder="What needs to be done?"
value={draft()}
onInput={(event) => setDraft(event.currentTarget.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
<For each={items()}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
);
}{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Related pages
- Write-tests challenges: authoring guide, file categories and validation
- Solving a write-tests challenge: what the solver sees and how results are scored
- Create a code challenge