Skip to content

testing

xero edited this page Nov 21, 2025 · 12 revisions

Testing

teXt0wnz uses a comprehensive three-part testing strategy: unit tests with Vitest, DOM/component tests with Testing Library, and end-to-end tests with Playwright.

Important

view the latest unit coverage report view the latest e2e testing report

Testing Strategy Overview

Triple-Headed Testing

Vitest (Unit Tests)

  • Test individual modules and functions in isolation
  • Fast execution
  • High coverage of business logic
  • Mock external dependencies

Testing Library (DOM/Component Tests)

  • Test DOM manipulation and user interactions
  • Focus on behavior over implementation
  • User-centric testing approach
  • Integration between UI components

Playwright (E2E Tests)

  • Test complete user workflows in real browsers
  • Cross-browser compatibility testing
  • Visual regression detection
  • Realistic user scenarios

Directory Structure

tests/
β”œβ”€β”€ unit/                          # Vitest unit tests
β”‚   β”œβ”€β”€ canvas.test.js             # client tests in the root
β”‚   β”œβ”€β”€ fileLoad.test.js          # File I/O - Load and Save
β”‚   β”œβ”€β”€ freehandAdvanced.test.js  # Drawing tools - Advanced Tools
β”‚   β”œβ”€β”€ uiBasic.test.js           # UI - Basic Utilities
β”‚   β”œβ”€β”€ ...
β”‚   └── server/                    # Server tests nested
β”‚       β”œβ”€β”€ config.test.js
β”‚       β”œβ”€β”€ fileio.test.js
β”‚       └── ...
β”œβ”€β”€ dom/                           # Testing Library tests
β”‚   β”œβ”€β”€ canvas.test.js
β”‚   β”œβ”€β”€ fontPreview.test.js
β”‚   └── ...
β”œβ”€β”€ e2e/                           # Playwright E2E tests
β”‚   β”œβ”€β”€ canvas.spec.js
β”‚   β”œβ”€β”€ tools.spec.js
β”‚   └── ...
β”œβ”€β”€ results/                       # Test results (gitignored)
β”‚   β”œβ”€β”€ coverage/                  # Coverage reports
β”‚   β”œβ”€β”€ e2e/                       # E2E artifacts
β”‚   └── playwright-report/         # Playwright HTML report
β”œβ”€β”€ canvasShim.js                  # Canvas API shim for tests
└── setupTests.js                  # Test environment setup

Vitest (Unit Testing)

Configuration

File: vitest.config.js

import { defineConfig } from 'vitest/config';

const ignore = [
	'*.config.js',
	'banner',
	'dist',
	'docs',
	'session',
	'node_modules',
	'tests/e2e/**',
];

export default defineConfig({
	test: {
		environment: 'jsdom',
		setupFiles: ['./tests/canvasShim.js', './tests/setupTests.js'],
		globals: true,
		isolate: true, // Ensure clean state between tests
		maxWorkers: 1, // Single thread to avoid memory multiplication
		exclude: ignore,
		coverage: {
			enabled: true,
			reporter: ['text', 'html'],
			reportsDirectory: 'tests/results/coverage',
			exclude: ignore,
		},
	},
});

Note

Vitest v4 deprecated threads/maxThreads in favor of maxWorkers/isolate Set maxWorkers: 1 and isolate: true for single-threaded execution.

Running Unit Tests

Run all tests:

bun test:unit
# or
npm run test:unit

Run tests with coverage:

npx vitest run --coverage
# or
bunx vitest run --coverage

Run tests in watch mode:

npx vitest
# or
bunx vitest

Run specific test file:

npx vitest tests/unit/canvas.test.js
npx vitest tests/unit/server/config.test.js

Run tests matching a pattern:

npx vitest canvas
npx vitest server

Run tests with UI:

npx vitest --ui

Test Coverage

Current status:

  • Overall: ~45% statement coverage
  • Client modules: 45% average
  • Server modules: 51% average

Coverage goals:

  • Critical modules: 80%+ coverage
  • General modules: 60%+ coverage
  • Utility functions: 90%+ coverage

View coverage report:

# After running tests with --coverage
open tests/results/coverage/index.html      # macOS
xdg-open tests/results/coverage/index.html  # Linux
start tests/results/coverage/index.html     # Windows

V8 Coverage Improvements in Vitest v4

Vitest v4 uses AST-based coverage analysis for more accurate results.

  • More precise coverage mapping with fewer false positives
  • Lines without runtime code are automatically excluded
  • coverage.ignoreClassMethods now supported by V8 provider
  • New ignore comment syntax:
    • Single line: /* v8 ignore next */
    • Code block: /* v8 ignore start */ and /* v8 ignore stop */

Coverage ignore examples

Ignore a single line:

/* v8 ignore next */
if (DEBUG) console.log('debug info');

Ignore a block:

/* v8 ignore start */
const debug = msg => {
	console.log('debug info:', msg);
};
/* v8 ignore stop */

Test Files

Client Modules

canvas.test.js - Canvas rendering and manipulation

  • Canvas initialization and setup
  • Drawing operations
  • Undo/redo functionality
  • Image data manipulation
  • Dirty region tracking
  • Mirror mode functionality
  • XBin font/palette handling

fileLoad.test.js - File I/O: Load and Save operations

  • Load module functionality
  • Save module operations
  • Data type conversions
  • Basic file format handling
  • ANSI and binary file support

fileFormats.test.js - File I/O: Formats and parsing

  • Binary data handling
  • Error handling
  • File class internal logic
  • SAUCE record processing
  • ANSI parsing engine
  • UTF-8 processing

fileAdvanced.test.js - File I/O: Advanced operations

  • Advanced save operations
  • Data conversion functions
  • File format edge cases
  • Internal data processing
  • PNG image generation
  • Font name conversions

font.test.js - Font loading and rendering

  • Font loading from images
  • XB font data parsing
  • Glyph rendering
  • Letter spacing handling
  • Alpha channel rendering
  • Error handling
  • Font dimension validation

freehandPanels.test.js - Drawing tools: Panels and cursors

  • Panel cursor creation
  • Floating panel palette
  • Floating panel management
  • Brush controller
  • Half-block controller
  • Shading controller and panel
  • Character brush panel

freehandShapes.test.js - Drawing tools: Shapes and fill

  • Fill controller
  • Shapes controller
  • Line controller
  • Square controller (outline and fill modes)
  • Circle controller
  • Shape drawing algorithms

freehandAdvanced.test.js - Drawing tools: Advanced tools

  • Attribute brush controller
  • Selection tool
  • Sample tool (color picker)
  • Line drawing algorithms
  • Panel state management
  • Event handling edge cases
  • Memory management patterns

keyboard.test.js - Keyboard input and shortcuts

  • Keyboard mode toggle
  • Text input handling
  • Arrow key navigation
  • Shortcut key handling
  • Cursor movement
  • Special key handling

main.test.js - Application initialization

  • Module initialization
  • Event listener setup
  • State management
  • Integration tests

network.test.js - Network and collaboration

  • WebSocket connection
  • Message handling
  • Chat functionality
  • User session management
  • Drawing synchronization

palette.test.js - Color palette management

  • Default palette creation
  • Color selection
  • RGB conversion
  • Ice colors support
  • Palette updates

state.test.js - Global state management

  • State initialization
  • State updates
  • Font management
  • Palette management
  • Canvas state

toolbar.test.js - Toolbar interactions

  • Tool selection
  • Tool switching
  • Toolbar state management
  • UI updates

uiBasic.test.js - User interface: Basic utilities

  • DOM utilities ($, $$, createCanvas)
  • Setting toggles
  • Event listener functions (onClick, onReturn, onFileChange)
  • Position info display
  • Modal functions
  • Undo/redo functionality

uiControls.test.js - User interface: Controls and controllers

  • Toggle button creation
  • Generic controller pattern
  • Byte enforcement for text fields
  • WebSocket UI updates
  • Fullscreen toggle
  • Modal controller management

uiComponents.test.js - User interface: Components

  • Viewport tap interactions
  • Paint shortcuts system
  • Resolution controller
  • Drag and drop controller
  • Menu controller

uiModals.test.js - User interface: Advanced utilities

  • Additional DOM utilities ($$$)
  • Grid creation and management
  • Tool preview rendering
  • Font selection interface
  • WebSocket connectivity UI

utils.test.js - Utility functions

  • Helper functions
  • Data manipulation
  • Format conversions

worker.test.js - Web Worker

  • Worker message handling
  • Data processing algorithms
  • Block deduplication logic
  • Message processing

xbinPersistence.test.js - XBin format persistence

  • Embedded font handling
  • Palette persistence
  • File roundtrip testing

Note

Test File Organization Best Practices

Test files should be small and focused on testing specific features rather than entire sections of the editor. Each test file should:

  • Focus on a single module or feature area (e.g., file-load.test.js for loading, file-formats.test.js for parsing)
  • Keep file size under ~500 lines for maintainability
  • Group related functionality logically (e.g., UI controls vs. UI components)
  • Make it easy to locate and run tests for specific features
  • Enable faster test execution when working on specific areas

Large monolithic test files (1000+ lines) should be split into focused, feature-specific files as demonstrated by:

  • ui-*.test.js - Split by UI feature groups (basic, controls, components, modals)
  • file-*.test.js - Split by operation type (load, formats, advanced)
  • freehand-*.test.js - Split by tool categories (panels, shapes, advanced)

Server Modules

server/config.test.js - Server configuration

  • Configuration parsing
  • Default values
  • Validation

server/fileio.test.js - Server file operations

  • SAUCE record creation and parsing
  • Binary data conversions
  • File format validation
  • Canvas dimension extraction
  • Data type conversions

server/main.test.js - Server main module

  • Module structure validation
  • Configuration integration
  • Component exports
  • Dependency integration

server/server.test.js - Express server

  • Server initialization
  • Route handling
  • Middleware setup
  • SSL configuration

server/text0wnz.test.js - Collaboration engine

  • Session management
  • User tracking
  • Canvas state synchronization
  • Message broadcasting

server/utils.test.js - Server utilities

  • Helper functions
  • Data validation
  • Format conversions

server/websockets.test.js - WebSocket handlers

  • Connection handling
  • Message routing
  • Session cleanup
  • Error handling

Writing Unit Tests

Important

Vitest v4 requires regular functions (not arrow functions) for constructor mocks

When mocking constructors, you must use the function keyword or class syntax. Arrow functions will cause a "not a constructor" error.

For more details, see the Vitest v4 migration guide.

Since the project's ESLint configuration prefers arrow functions, you may see lint warnings when using regular functions for constructor mocks.

Best practice: Only disable ESLint's prefer-arrow-callback or func-names rules locally, around the specific constructor mock, rather than for the entire file. For example:

// eslint-disable-next-line prefer-arrow-callback
const MyMock = function () {
	/* ... */
};

Or, if you need to disable func-names for anonymous constructors:

// eslint-disable-next-line func-names
const MyMock = function () {
	/* ... */
};

Test structure:

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { functionToTest } from '../../src/js/client/module.js';

describe('Module Name', () => {
	beforeEach(() => {
		// Setup before each test
		vi.clearAllMocks();
	});

	afterEach(() => {
		// Cleanup after each test
		vi.restoreAllMocks();
	});

	describe('Function Group', () => {
		it('should do something specific', () => {
			// Arrange
			const input = 'test data';

			// Act
			const result = functionToTest(input);

			// Assert
			expect(result).toBe('expected output');
		});

		it('should handle edge cases', () => {
			expect(() => functionToTest(null)).toThrow();
		});
	});
});

Mocking:

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { functionToTest } from '../../src/js/client/module.js';

// Example of a constructor mock using a regular function:

// eslint-disable-next-line prefer-arrow-callback
const MyConstructorMock = function () {
	// mock implementation
};

// Mock a regular function (arrow functions are OK for regular function mocks, but NOT for constructor mocks)
const mockFunction = vi.fn(() => 'mocked value');

// Mock a constructor - MUST use function keyword or class
global.FileReader = vi.fn(function () {
	return mockReaderInstance;
});

// OR use a class for constructor mocks
global.FileReader = vi.fn(
	class MockFileReader {
		constructor() {
			this.result = null;
		}
		readAsArrayBuffer() {
			// mock implementation
		}
	},
);

// Spy on a method
const spy = vi.spyOn(object, 'method');

Best practices:

  1. Test isolation - each test should be independent
  2. Clear names - describe expected behavior
  3. Arrange-Act-Assert - structure tests clearly
  4. Mock dependencies - test in isolation
  5. Test edge cases - include error conditions
  6. Avoid implementation details - test behavior
  7. Keep tests fast - unit tests should run quickly
  8. Clean up - remove event listeners, restore mocks

Testing Library (DOM/Component Testing)

Setup

Testing Library is integrated with Vitest for DOM manipulation testing.

Configuration: tests/setupTests.js

import '@testing-library/jest-dom';

DOM Testing Patterns

Testing DOM manipulation:

import { screen } from '@testing-library/dom';

it('should update element', () => {
	const element = document.createElement('div');
	updateElement(element, 'new text');
	expect(element).toHaveTextContent('new text');
});

Testing events:

import { fireEvent } from '@testing-library/dom';

it('should handle events', () => {
	const handler = vi.fn();
	const button = document.createElement('button');
	button.addEventListener('click', handler);

	fireEvent.click(button);

	expect(handler).toHaveBeenCalled();
});

Testing async code:

it('should load data asynchronously', async () => {
	const result = await loadData();
	expect(result).toBeDefined();
});

Playwright (End-to-End Testing)

Configuration

File: playwright.config.js

export default defineConfig({
	testDir: './tests/e2e',
	timeout: 30000,
	retries: 1,
	outputDir: 'tests/results/e2e',
	use: {
		baseURL: 'http://localhost:8060',
		viewport: { width: 1280, height: 720 },
		screenshot: 'only-on-failure',
	},
	webServer: {
		command: 'bun www',
		port: 8060,
		reuseExistingServer: !process.env.CI,
	},
	projects: [
		{ name: 'Chrome', use: { channel: 'chrome' } },
		{ name: 'Firefox', use: { browserName: 'firefox' } },
		{ name: 'WebKit', use: { browserName: 'webkit' } },
	],
});

Running E2E Tests

Prerequisites:

  1. Build the application:
bun bake
  1. Install browsers (first time only):
bun test:install
# or
npx playwright install

Run all E2E tests:

bun test:e2e
# or
npx playwright test

Run tests for specific browser:

npx playwright test --project=Chrome
npx playwright test --project=Firefox
npx playwright test --project=WebKit

Run specific test file:

npx playwright test tests/e2e/canvas.spec.js

Run tests in UI mode (interactive):

npx playwright test --ui

Run tests in headed mode (see browser):

npx playwright test --headed

Debug tests:

npx playwright test --debug

Test Results

Results are saved to:

  • HTML Report: tests/results/playwright-report/
  • JSON Results: tests/results/e2e/results.json
  • Videos/Screenshots: tests/results/e2e/

View HTML report:

npx playwright show-report tests/results/playwright-report

E2E Test Files

canvas.spec.js - Basic canvas functionality

  • Application loading
  • Canvas visibility and initialization
  • Canvas resizing
  • Mouse drawing interactions
  • Tool switching
  • Position information updates

tools.spec.js - Drawing tools functionality

  • Freehand drawing tool
  • Character tool
  • Brush tool
  • Line tool
  • Square tool
  • Circle tool
  • Selection tool
  • Fill tool
  • Undo/redo operations
  • Copy/paste operations

palette.spec.js - Color palette and character selection

  • Color palette visibility
  • Foreground/background color selection
  • ICE colors toggle
  • Keyboard shortcuts for colors
  • Sample tool (color picker)
  • Character palette selection

fileOperations.spec.js - File I/O and canvas operations

  • New document creation
  • File open dialog
  • Save options (ANSI, Binary, XBin, PNG)
  • SAUCE metadata fields
  • Canvas resize operations
  • ICE colors toggle
  • Font selection and changes
  • Canvas clearing

keyboard.spec.js - Keyboard shortcuts and keyboard mode

  • Undo/redo shortcuts
  • Tool selection shortcuts
  • Keyboard mode entry/exit
  • Arrow key navigation
  • Function key shortcuts
  • Copy/paste/cut shortcuts
  • Text input in keyboard mode
  • Home, End, PageUp, PageDown navigation

ui.spec.js - User interface elements

  • Main UI element visibility
  • Responsive layout
  • Position information display
  • Toolbar with drawing tools
  • File operations menu
  • Canvas settings controls
  • Font selection interface
  • Tool highlighting
  • Modal dialogs

editor-file-open-and-scroll.spec.js - File loading and viewport scrolling

  • Opening ANSI files via file input (docs/examples/ansi/x0-defcon25.ans)
  • PageDown/PageUp keyboard scrolling behavior
  • Mouse wheel scrolling of canvas viewport
  • Viewport scroll position tracking and validation

editor-keybinds.spec.js - Comprehensive keybind coverage

  • Tool activation shortcuts (K, F, B, N, A, M, G, I)
  • Navigation in keyboard mode (Arrow keys, Home, End, PageUp, PageDown)
  • Undo/redo operations (Ctrl+Z, Ctrl+Y)
  • Clipboard operations (Ctrl+C, Ctrl+V, Ctrl+X)
  • Function key character insertion (F1-F4)
  • Selection transformations (flip horizontal/vertical with [ and ])
  • Selection move mode (M key)

E2E Test Helpers

The E2E test suite includes reusable helper modules in tests/e2e/helpers/:

editorHelpers.js - Common editor utilities

  • waitForEditorReady(page, timeout) - Wait for canvas container and initialization
  • focusCanvas(page) - Click viewport to establish keyboard focus
  • getCanvasText(page) - Retrieve canvas text content (via window API if available)
  • getViewportScrollPosition(page) - Get current viewport scroll coordinates
  • getPositionInfo(page) - Read position info text from UI
  • getActiveTool(page) - Determine which tool is currently active

openFile.js - File loading utilities

  • openFile(page, filePath) - Open file using file input element and setInputFiles
  • openFileViaDragDrop(page, filePath) - Placeholder for drag-and-drop file opening
  • openFileViaURL(page, fileURL) - Open file via window.Load API (if exposed)

Example usage:

import { openFile } from './helpers/openFile.js';
import { getViewportScrollPosition } from './helpers/editorHelpers.js';

test('loads file and scrolls', async ({ page }) => {
	await openFile(page, 'docs/examples/ansi/x0-defcon25.ans');
	const before = await getViewportScrollPosition(page);
	await page.keyboard.press('PageDown');
	const after = await getViewportScrollPosition(page);
	expect(after.scrollTop).toBeGreaterThan(before.scrollTop);
});

Writing E2E Tests

Test structure:

import { test, expect } from '@playwright/test';

test.describe('Feature Name', () => {
	test.beforeEach(async ({ page }) => {
		await page.goto('/');
		await page.waitForSelector('#canvas-container', { timeout: 10000 });
		await page.waitForTimeout(1000);
	});

	test('should do something', async ({ page }) => {
		const element = page.locator('#my-element');
		await element.click();
		await page.waitForTimeout(300);

		await expect(element).toBeVisible();
	});
});

Best practices:

  1. Wait for elements - ensure elements are ready
  2. Use timeouts - add delays after interactions
  3. Flexible selectors - use multiple selector strategies
  4. Test isolation - each test should be independent
  5. Error handling - gracefully handle missing elements
  6. Clean up - use beforeEach and afterEach
  7. Meaningful assertions - test user-visible behavior

Continuous Integration

Tests run automatically on:

  • Every pull request
  • Every commit to main branch
  • Scheduled daily runs

GitHub Actions workflow:

  • Installs dependencies
  • Builds application
  • Runs unit tests
  • Runs E2E tests
  • Generates coverage reports
  • Archives test results

Test Metrics

Coverage Metrics

  • Statement Coverage: % of code statements executed
  • Branch Coverage: % of conditional branches taken
  • Function Coverage: % of functions called
  • Line Coverage: % of lines executed

Quality Goals

  • Critical modules: 80%+ coverage
  • General modules: 60%+ coverage
  • Utility functions: 90%+ coverage
  • E2E coverage: All major user workflows

Troubleshooting

Unit Tests

Tests timing out:

  • Increase timeout in vitest.config.js
  • Check for unhandled promises
  • Look for infinite loops

Mocks not working:

  • Ensure mocks are defined before imports
  • Check mock file paths
  • Verify mock implementation
  • Constructor mocks must use function keyword or class, not arrow functions
  • Add /* eslint-disable prefer-arrow-callback */ at the top of test files

"is not a constructor" errors:

  • This occurs when using arrow functions with vi.fn() for constructor mocks
  • Change vi.fn(() => ...) to vi.fn(function () { ... }) for constructors
  • Or use vi.fn(class MockClass { ... }) syntax
  • See the Mocking section for examples

Memory leaks:

  • Clean up event listeners in afterEach
  • Clear large objects after use
  • Use --no-coverage for faster iterations

Flaky tests:

  • Avoid timing-dependent tests
  • Mock Date.now() for time-based tests
  • Ensure proper cleanup between tests

E2E Tests

Tests fail with connection refused:

  • Ensure application is built: bun bake
  • Check web server starts automatically
  • Verify port 8060 is available

Browsers not installed:

  • Run: bun test:install
  • Or: npx playwright install --with-deps

Tests timeout:

  • Increase timeout in playwright.config.js
  • Add longer waits in specific tests
  • Check if application loads slowly

Tests are flaky:

  • Add explicit waits (waitForTimeout, waitForSelector)
  • Use waitForLoadState('networkidle')
  • Increase retry count in config

Screenshots/videos not captured:

  • Check outputDir in playwright.config.js
  • Verify write permissions in tests/results/
  • Ensure tests actually fail (screenshots only on failure by default)

Resources

Documentation

Guides

Additional Resources

See Also

Clone this wiki locally