Mock Sublime is a lightweight, open‑source library designed to provide mock implementations of the Sublime Text API for use in automated tests of Sublime Text plugins and extensions. By abstracting the platform‑specific API, Mock Sublime enables developers to write unit and integration tests that run in a headless environment, without requiring an actual instance of the Sublime Text editor. The library was created to address the lack of testing support in the official Sublime Text ecosystem, where plugins are traditionally developed and validated manually within the editor.
Introduction
Purpose and Scope
Mock Sublime offers a set of mock classes and helper functions that mimic the behavior of key Sublime Text components, including Window, View, Buffer, and Command objects. The library focuses on reproducing the essential API surface used by most plugins, providing configurable responses to method calls and allowing test authors to simulate user actions such as opening files, editing text, and invoking commands. It is intentionally minimalistic; features beyond the core API surface are omitted unless they are widely used in plugin code.
Target Audience
The primary users of Mock Sublime are plugin developers, continuous integration pipelines, and educational projects that demonstrate the development of Sublime Text packages. The library is written in JavaScript/TypeScript, making it compatible with Node.js versions 12 and above. By offering both CommonJS and ES Module builds, it can be integrated into a variety of build systems, including Webpack, Rollup, and Jest test runners.
Relationship to the Sublime Text Ecosystem
Mock Sublime is not an official extension or part of the Sublime Text distribution. Instead, it relies on the public API documentation available at Sublime Text API Reference. The library is published under the MIT license and hosted on GitHub, where it follows a conventional open‑source development workflow. Because it is decoupled from the editor itself, it can be used in environments where Sublime Text is unavailable, such as Docker containers or cloud CI services.
Core Advantages
- Deterministic Testing: By mocking API calls, tests can be written to run deterministically, avoiding flakiness caused by UI state.
- Speed: Running tests against mocked objects is significantly faster than launching a full editor instance.
- Isolation: Plugins can be unit tested in isolation from the editor, making it easier to catch logic errors early.
- Extensibility: The library exposes hooks that allow developers to extend or customize mock behavior for advanced scenarios.
Limitations
Mock Sublime does not emulate the full rendering or event loop of Sublime Text. Features that depend on the editor's graphical interface or on internal mechanisms not documented in the public API are outside its scope. Consequently, integration tests that require real UI interaction should still be performed manually or with additional tooling such as Sublime UI Automation, which is a separate project.
History and Background
Origins
The idea for Mock Sublime emerged in 2019 during a discussion at the annual SublimeConf conference. The speaker, Alex Martinez, highlighted the challenges of testing Sublime Text plugins due to the lack of a stable headless API. The community responded with a call for a lightweight testing framework, leading to the creation of Mock Sublime in early 2020.
Initial Release
The first public release, version 0.1.0, was published on GitHub on March 3, 2020. It included stubs for Window and View classes, along with a basic command dispatcher. The release notes specified that the library was a proof of concept and encouraged contributors to submit pull requests for additional API coverage.
Maturation
Between 2020 and 2022, Mock Sublime grew steadily. Major milestones include:
- v0.3.0: Added support for
EventListenercallbacks, enabling simulation of key press events. - v0.5.0: Introduced TypeScript typings, allowing static analysis and better editor integration for developers using TypeScript.
- v1.0.0: Delivered a full mock of the
TextInputHandlerinterface, permitting plugins that rely on user prompts to be tested automatically. - v2.0.0: Provided configuration options to customize buffer behaviors, such as simulating read‑only files or unsaved changes.
Adoption
By late 2021, over 200 plugins had been tested using Mock Sublime in CI pipelines. Notable adopters include the PackageControl testing harness and the Sublime Test Marketplace plugin. The library’s popularity is reflected in its GitHub traffic and the number of forks, with more than 1,500 weekly visitors as of 2023.
Governance
Mock Sublime is governed by a core team composed of Martinez, Maya Chen, and Ravi Patel. Decisions are made through issue discussion and pull request reviews. The project adheres to the Contributor Covenant Code of Conduct, ensuring a welcoming environment for new contributors.
Current Status
As of September 2023, the library is in active development. The roadmap includes support for asynchronous command execution, improved buffer diffing algorithms, and optional integration with ESLint to enforce API compliance. The next major release, 3.0.0, is slated for December 2023 and will include a comprehensive test suite covering 90% of the public API surface.
Architecture
Modular Design
Mock Sublime is structured around a modular architecture that separates concerns into distinct packages. The core package @mock-sublime/core implements the foundational mock classes, while optional adapters provide compatibility layers for different test runners. The following sub‑modules are defined:
MockWindow– simulates window operations such as opening and closing views.MockView– emulates file views, including content, cursor positioning, and selection management.MockCommandDispatcher– handles registration and execution of commands.MockBuffer– provides a lightweight representation of the underlying text buffer.MockEventEmitter– implements an event system for simulating event listeners.
Dependency Graph
The library has minimal external dependencies to keep it lightweight. The only runtime dependencies are GitHub for documentation consumption and Node.js for execution. During development, developers can optionally add Jest or Mocha as test runners, ESLint for linting, and TypeScript for type safety.
Mock Construction Pipeline
When a test suite is executed, the MockSublime entry point is imported, creating a new mock environment instance. The following pipeline describes the creation of the mock environment:
- Initialize the
MockEnvironmentwith default configurations. - Register mock views and windows using factory methods.
- Inject the command dispatcher into plugin modules via dependency injection.
- Run the test suite, capturing command execution flow.
- Assert on mock state changes, such as buffer content or selection ranges.
Command Dispatching Mechanism
The command dispatcher in Mock Sublime closely follows the convention used by Sublime Text plugins, where commands are invoked through window.run_command or view.run_command. The dispatcher accepts a command name and an optional arguments object, then resolves the command to a class or function registered in the mock environment. The dispatcher also tracks command history, enabling tests to verify that specific commands were executed with the expected arguments.
Configuration API
Configuration is performed via a JSON schema that describes default view content, window layout, and command behaviors. Tests can override these defaults at runtime by passing a configuration object to the MockEnvironment constructor. The configuration system supports nested properties, allowing fine‑grained control over individual mock instances. For example, a test can specify that a particular view should return a pre‑defined file name and content when file_name is called, while another view can simulate an unsaved buffer.
Architecture
Core Modules
The Mock Sublime core consists of five primary modules:
environment.ts– orchestrates the creation of mock windows and views.window.ts– provides aMockWindowclass that manages view collections.view.ts– implements theMockViewclass, including text manipulation methods.command.ts– defines a baseMockCommandclass and the dispatcher logic.buffer.ts– contains a lightweight representation of the underlying buffer.
Each module exports both the class itself and a factory function for creating instances with pre‑configured state. The design is intentionally composable; a plugin that requires only the View API can import MockView directly, without pulling in the entire window subsystem.
Event Handling Layer
Mock Sublime implements an event system inspired by Node.js EventEmitter. The MockEventEmitter class exposes methods such as on, once, and emit, allowing tests to subscribe to events like on_modified or on_close. The event system is purely synthetic; it does not connect to any real editor event loop but instead triggers callbacks when the corresponding mock methods are called. For example, invoking view.set_scratch will emit an on_modified event if a listener is registered.
Asynchronous Support
Because many Sublime Text plugins perform asynchronous operations, Mock Sublime provides asynchronous versions of common methods. For instance, view.run_command_async returns a Promise that resolves once the command logic completes. The library also supports set_timeout callbacks, allowing tests to simulate delayed responses from the editor.
TypeScript Integration
TypeScript definitions for Mock Sublime are distributed alongside the source code, leveraging the @types/sublime-text package (a community‑maintained typings repository). These definitions ensure that plugin code written in TypeScript compiles correctly against the mock API. The build system automatically generates declaration files (.d.ts) for each release, making it straightforward to integrate into larger TypeScript projects.
Testing Utilities
In addition to the core mock classes, Mock Sublime ships a suite of utility functions designed to streamline test authoring. Functions such as createMockView, setViewContent, and assertViewText provide common patterns for manipulating and inspecting view state. These utilities are heavily documented in the project's README, with inline examples that showcase typical usage patterns in Jest and Mocha.
Key Concepts
Mocking vs. Stubbing
Mock Sublime distinguishes between mocking and stubbing by offering two separate paradigms. Stubbing provides a static response to a method call, while mocking allows assertions on the call sequence and arguments. For example, a stub for view.file_name can simply return a string, whereas a mock can record each invocation and verify that the plugin requested the file name only once.
Spies and Call Tracking
Spies in Mock Sublime are implemented through the MockEventEmitter. When a plugin registers an event listener, the spy records each emit call along with the payload. Test suites can then use helper methods like getSpyCalls to inspect the call history. This feature is particularly useful for validating complex interactions where the order of events is critical.
Command History
The command dispatcher maintains a global history object that maps command names to arrays of argument sets. Tests can inspect this history to assert that specific commands were invoked in a particular order. This functionality is especially valuable for plugins that orchestrate multiple commands as part of a workflow.
Buffer Diffing Algorithms
Mock Sublime’s MockBuffer class implements a diffing algorithm that identifies changes between successive states. The algorithm tracks line numbers, insertion points, and deletions, enabling tests to assert that the plugin performed a specific text replacement. The diffing logic is optimized for small buffers, ensuring minimal overhead in unit tests.
Configuration Scopes
Configuration can be scoped to the entire environment, a single window, or a single view. The mock environment accepts a scopes object, wherein each scope defines a default state. For instance, setting view.is_read_only = true within the view scope will cause all subsequent view instances to default to read‑only mode unless overridden.
Asynchronous Command Lifecycle
Asynchronous commands in Mock Sublime are modeled after the Sublime Text run_command_async pattern. The mock dispatcher wraps the command logic inside a Promise, allowing the test to await command completion before performing assertions. The mock environment also provides set_timeout to delay command execution, mimicking the editor’s asynchronous behavior.
Integration with Linting Tools
Developers can optionally integrate Mock Sublime with ESLint by adding the eslint-plugin-sublime plugin. This integration enforces that plugin code does not call any unsupported API methods, helping maintain compatibility with the mock environment. Tests can then fail early if a plugin attempts to use an API method that is not present in Mock Sublime.
Key Concepts
API Compliance Checks
Mock Sublime provides a compliance checker that verifies plugin modules against the mock API. The checker uses the @mock-sublime/api-checker tool to introspect exported functions and ensure they align with the mocked interface. The checker produces a report that highlights missing methods or mismatched signatures.
Command Execution Mocking
When a plugin executes a command, Mock Sublime’s dispatcher captures the command name and arguments. The dispatcher can also be configured to simulate failures by throwing an exception or returning a rejected promise. Tests can then assert that the plugin handles these errors gracefully.
Environment Reset
After each test case, Mock Sublime automatically resets the mock environment to its default state. This reset is performed by calling environment.reset, which clears all mock views, windows, and command histories. Resetting the environment ensures that tests do not interfere with each other, maintaining isolation.
Integration with External Tools
Mock Sublime can be extended with adapters for ESLint and GitHub actions. These adapters allow tests to automatically trigger linting or pull request checks based on the mock environment’s state, enabling continuous integration pipelines that validate plugin code before deployment.
Testing Strategies
Typical testing strategies with Mock Sublime include:
- Unit tests that verify isolated view operations.
- Integration tests that simulate full command workflows.
- Behavior‑driven tests that use spies to validate interaction patterns.
- End‑to‑end tests that spin up a mock environment, execute a sequence of commands, and assert on final view state.
Testing Utilities
MockViewFactory
The MockViewFactory is a higher‑order function that generates MockView instances pre‑loaded with content. It accepts parameters such as fileName, content, and readOnly, returning a fully initialized view. The factory is implemented in mock-view-factory.ts and is exported under @mock-sublime/utils.
View Manipulation Helpers
Utilities like setViewCursor and insertTextAtCursor provide shorthand operations that reduce boilerplate in tests. These helpers internally call MockView methods, ensuring consistent behavior across different test frameworks.
Assertion Helpers
The library ships with a set of assertion helpers that can be used with Jest or Mocha. Examples include:
expectViewText(view, expectedText)– compares view buffer content.expectViewSelection(view, start, end)– asserts cursor and selection ranges.expectCommandExecuted(dispatcher, commandName, args)– verifies that a command was executed with specified arguments.
These helpers are designed to be chainable, allowing test writers to write expressive assertions such as expect(view).toHaveText('foo') or expect(dispatcher).toHaveExecuted('rename_file', {old: 'a', new: 'b'}).
Timeout and Delayed Execution Helpers
When a plugin schedules a set_timeout callback, tests can use mock-sublime/utils/wait to pause execution until the callback completes. This function accepts a timeout duration and returns a Promise, enabling asynchronous test flows that mirror real editor behavior.
Test Runner Adapters
Adapters for Jest and Mocha are provided to simplify integration:
@mock-sublime/jest– exposes abeforeEachhook that automatically initializes the mock environment.@mock-sublime/mocha– provides abeforeEachandafterEachpair to reset the environment.
These adapters also expose custom matchers for Jest (toHaveExecutedCommand) and assertion hooks for Mocha.
Testing Utilities
Factory Functions
Each mock class comes with a factory function that accepts a configuration object. For example, createMockWindow can be called with {views: [view1, view2]} to create a window with two pre‑loaded views. These factory functions reduce boilerplate, enabling tests to focus on the behavior rather than setup.
Assertion Patterns
Common assertion patterns are provided as static methods on the mock classes. For example, MockView.assertText compares the current buffer content to an expected string, while MockEnvironment.assertCommandHistory validates that the sequence of executed commands matches an expected list.
Event Listener Assertions
Tests can assert that event listeners were correctly triggered by inspecting the MockEventEmitter’s internal call log. The library offers a assertEventTriggered helper that verifies that a specific event was emitted with the correct payload.
Integration with CI
Mock Sublime’s utilities are designed to work seamlessly with CI systems. In a typical GitHub Actions workflow, the steps include installing dependencies, running tests via Jest, and publishing coverage reports. The test runner can be configured to use the jest or mocha adapters, which automatically initialize and reset the mock environment at the start of each test run.
Testing Asynchronous Commands
When testing asynchronous commands, the test can use await dispatcher.runCommandAsync('my_command', {foo: 'bar'}) to wait for completion. The mock environment tracks execution time, allowing tests to assert that the command took at least a specified duration or that a timeout occurred.
Testing Utilities
MockEnvironment API
The MockEnvironment class is the primary interface for creating and managing a mock Sublime environment. Its constructor accepts a configuration object, and it exposes methods to:
- Create windows via
createWindow. - Open views with
openView. - Register commands with
registerCommand. - Reset the environment with
reset. - Retrieve the command history via
getCommandHistory.
In tests, developers instantiate a new MockEnvironment before each test suite and reset it afterward to ensure isolation.
Command Dispatcher
The CommandDispatcher is responsible for executing commands in the mock environment. It exposes methods such as executeCommand and executeCommandAsync that return promises when asynchronous commands are used. The dispatcher also logs command history, enabling verification of command sequences.
MockView Utilities
The MockView class has a set of static utilities that help with text manipulation and state verification. These include methods like insertText, replaceText, and deleteText, each returning a Promise for asynchronous compatibility.
Command History Logger
The CommandHistoryLogger captures the sequence of command executions. Tests can query it via getHistory to retrieve an array of command objects with name, arguments, and timestamps.
Diff Helper
The diff function computes the difference between two mock buffer states. It returns an array of changes, each describing added, removed, or modified lines. Tests can use this helper to assert that a command produced the expected changes.
Assertion Helper Functions
Assertion helpers include Mock Sublime offers ready‑made test cases for typical command scenarios. In a test file, developers import the necessary utilities and create a mock dispatcher, then run commands and validate outcomes. The utilities also provide assertViewText, assertSelection, and assertCommandExecuted. These functions accept the mock environment and expected values, performing assertions using Jest or Mocha APIs. They are designed to be chainable, such that tests can read naturally: assertViewText(view, 'expected text')
Command Execution Test Cases
simulateFailure to test error handling in commands.Command Execution Mocking
Async Command Execution
We need to include a comprehensive explanation. Provide overview, use cases, integration, examples. Mention testing utilities, diffing, etc. Include code example. Provide sections. Provide conclusion.
Let's create a structured section. We need to be comprehensive but concise. Let's write a section that includes:
Let's write.## 5. Command‑Execution Mocking
Command execution is the lifeblood of a Sublime Text plugin. A well‑designed test harness must therefore give you full visibility into every invocation - its arguments, the order in which they fire, and the state changes they trigger.
@mock-sublime provides a Command Dispatcher that mimics the real runcommand / runcommand_async APIs while allowing you to introspect, modify, and verify the call‑site. Below is a deep‑dive into how to make the most of it.
| Feature | What it is | Why it matters |
|--------|------------|----------------|
| `Dispatcher.executeCommand(name, args)` | Synchronously runs a command. | Tests that exercise simple flows quickly. |
| `Dispatcher.executeCommandAsync(name, args)` | Returns a Promise that resolves after the command’s logic completes. | Mimics Sublime’s async behaviour; lets you `await` in tests. |
| `Dispatcher.history` | Keeps an ordered list of all executed commands with timestamps. | Assert call order, detect unexpected commands, generate reports. |
| `Dispatcher.mockError(name, errorFn)` | Configures a command to throw / reject. | Test error‑handling paths. |
| `Dispatcher.register(cmdObj)` | Registers a command implementation. | You can add stubs or mocks on the fly. |
5.1 Core API
ts
// 1️⃣ Initialise the dispatcher for a test case
import {MockEnvironment, CommandDispatcher} from '@mock-sublime/env';
const env = new MockEnvironment(); // Creates a fresh env
const dispatcher = new CommandDispatcher(env);
// 2️⃣ Register a real command implementation
dispatcher.register({
name: 'rename_file',
fn: async (oldPath: string, newPath: string) => {
},
});
// 3️⃣ Execute synchronously
dispatcher.executeCommand('rename_file', {oldPath: 'a.txt', newPath: 'b.txt'});
// 4️⃣ Execute asynchronously
await dispatcher.executeCommandAsync('async_cleanup', {keep: true});
// 5️⃣ Inspect the history
const history = dispatcher.history; // [{name:'rename_file', args:{...}, time:...}, {...}]
expect(history[0].name).toBe('rename_file');
> **Tip:** The dispatcher automatically records the **timestamp** of each call. You can therefore assert that a command took longer than a given duration or that it was executed before another command.
// Simulated async rename logic
await env.sleep(50); // pretend to touch FS
env.renameFile(oldPath, newPath);5.2 Simulating Failures
Testing error handling is as important as testing success paths. `Dispatcher.mockError` lets you trigger a failure scenario without changing the command implementation.
ts
// Force `cleanup` to throw a runtime error
dispatcher.mockError('cleanup', () => new Error('Cleanup failed'));
// Verify that the plugin caught it
await expect(dispatcher.executeCommandAsync('cleanup', {})).rejects.toThrow('Cleanup failed');
When you want to simulate *partial* failures - e.g., a command that rejects only after a timeout - combine `mockError` with `env.sleep()`.
ts
dispatcher.mockError('timeout_command', async () => {
await env.sleep(200); // simulate long delay
throw new Error('Timeout exceeded');
});
await expect(dispatcher.executeCommandAsync('timeout_command', {})).rejects
.toThrow('Timeout exceeded');
5.3 Command History & Ordering
Dispatcher.history is a simple array of { name, args, start, end }.
ts
// Suppose plugin P runs A → B → C
dispatcher.executeCommand('A', {});
dispatcher.executeCommandAsync('B', {});
dispatcher.executeCommand('C', {});
// After the run
const names = dispatcher.history.map(e => e.name);
expect(names).toEqual(['A', 'B', 'C']); // Order check
// Verify that B started after A finished
const [a, b, c] = dispatcher.history;
expect(b.start).toBeGreaterThan(a.end);
start / end timestamps give you insight into overlapping async calls.5.4 Diffing Text Buffers
If a command modifies the buffer, you can assert the exact changes with `env.diff()`.
ts
// Buffer before
env.openView({fileName: 'demo.txt', content: 'foo\nbar\nbaz'});
// Run a replace command
dispatcher.executeCommand('replace', {find: 'bar', replace: 'qux'});
// After the run
const diff = env.diff(); // [{type:'replace', line:1, old:'bar', new:'qux'}]
expect(diff[0].type).toBe('replace');
expect(diff[0].new).toBe('qux');
5.5 Integration with Test Runners
Both Jest and Mocha receive adapters that automatically wrap the dispatcher in hooks:
| Runner | Hook | What it does |
|--------|------|--------------|
| Jest | `beforeEach` | `env.reset()` + `dispatcher = new CommandDispatcher(env)` |
| Mocha | `beforeEach` / `afterEach` | Same as Jest but with an explicit reset in `afterEach` |
ts
// Jest example
import {jestAdapter} from '@mock-sublime/jest';
jestAdapter.beforeEach(); // sets env, dispatcher
test('rename_file command', async () => {
await dispatcher.executeCommandAsync('rename_file', {...});
// assertions …
});
5.6 CI‑Friendly Patterns
When running tests in GitHub Actions, GitLab CI, or Azure Pipelines, you typically:
yaml
npm ci).jest --runInBand (or mocha) which will auto‑initialize the dispatcher via the adapters.codecov or similar.
uses: actions/upload-artifact@v2
with:
The report is generated by `dispatcher.report()` that dumps the entire history as JSON.
name: cmd-report
path: ./test-results/command-history.json5.7 Common Pitfalls & Recommendations
| Pitfall | Fix / Recommendation |
|---------|----------------------|
| **Dispatcher never receives a command** – because the plugin registers the command only during runtime. | Register in a `beforeEach` hook or use `dispatcher.register()` before you call. |
| **Async commands never resolve** – forgetting `await env.sleep()` or returning a Promise. | Always return `Promise5.7 Putting It All Together
ts
import {MockEnvironment, CommandDispatcher} from '@mock-sublime/env';
import {jestAdapter} from '@mock-sublime/jest';
jestAdapter.beforeEach(); // env & dispatcher set up
describe('File utilities', () => {
it('should rename a file and report the operation', async () => {
env.createFile('src/main.js', 'console.log("hi")');
dispatcher.register({
name: 'rename_file',
fn: async ({oldPath, newPath}) => {
await env.sleep(20);
env.renameFile(oldPath, newPath);
},
});await dispatcher.executeCommandAsync('rename_file', {
oldPath: 'src/main.js',
newPath: 'src/app.js',
});expect(env.fileExists('src/app.js')).toBe(true);
expect(dispatcher.history[0].name).toBe('rename_file');
});
it('should handle cleanup failure', async () => {
// Check that the diff reports the rename
const historyReport = dispatcher.report(); // JSON file for CI
expect(historyReport[0].name).toBe('rename_file');
});
});
---
dispatcher.mockError('cleanup', () => new Error('Failed to cleanup'));
await expect(dispatcher.executeCommandAsync('cleanup', {})).rejects
.toThrow('Failed to cleanup');Take‑away
With this foundation you can confidently verify that **every command your plugin offers behaves exactly as you intend - under all circumstances.**
No comments yet. Be the first to comment!