Generic testing framework for Adobe UXP plugins with automated test execution
A standalone, reusable testing framework that any UXP plugin can use for automated testing.
✅ Vitest Compatibility - Write tests in modern vitest syntax (Architecture 2.0) ✅ 16 Comprehensive Assertion Methods - assertEqual, assertThrows, assertIncludes, and more ✅ Terminal-to-Photoshop Automation - Run tests from command line via Socket.IO bridge ✅ Dual Testing Approach - Fast Node.js unit tests + real UXP integration tests ✅ Self-Hosting Meta-Tests - Framework tests itself to ensure correctness ✅ Test Organization - describe blocks, lifecycle hooks (beforeEach, afterEach) ✅ Zero Production Dependencies - Core framework is pure JavaScript
git clone https://github.com/electrosaur-labs/uxp-test-runner.git
cd uxp-test-runner
npm installnpm testResult: 44 tests pass (17 adapter + 27 pixel assertions) in ~300ms ⚡
Terminal A - Start Proxy Server:
npm run proxyTerminal B - Build and Load Plugin:
npm run build:devThen load dist/manifest.json in UXP Developer Tool
Terminal B - Run Automated Tests:
npm run test:uxpResult: Tests run automatically in Photoshop via WebSocket bridge 🎉
# Fast Node.js unit tests
npm test # 63 tests, 36ms
# Full UXP integration tests (requires Photoshop + proxy running)
npm run test:uxp # 58 tests, 216ms
# Run both
npm run test:all- Plugins → UXP Test Runner → Run Tests
- Check console for detailed output
- Alert dialog shows summary
runner.test("basic arithmetic", () => {
TestRunner.assertEqual(1 + 1, 2);
TestRunner.assert(true, "Should pass");
});runner.describe("Math Operations", () => {
runner.test("addition works", () => {
TestRunner.assertEqual(5 + 3, 8);
});
runner.test("subtraction works", () => {
TestRunner.assertEqual(10 - 4, 6);
});
});runner.describe("Database Tests", () => {
let db;
runner.beforeEach(() => {
db = createTestDatabase();
});
runner.afterEach(() => {
db.cleanup();
});
runner.test("can insert records", () => {
db.insert({ name: "test" });
TestRunner.assertEqual(db.count(), 1);
});
});runner.test("async operation", async () => {
const result = await fetchData();
TestRunner.assertEqual(result.status, "success");
});You can now write tests using modern vitest syntax! The adapter layer translates vitest's expect() API to TestRunner assertions.
import { describe, test, expect } from 'vitest';
describe("Math Operations", () => {
test("addition works", () => {
expect(2 + 2).toBe(4);
expect([1, 2, 3]).toContain(2);
});
test("async operations", async () => {
const result = await fetchData();
expect(result).toEqual({ status: "success" });
});
});Run vitest tests:
npm run test:vitest # Run once
npm run test:vitest:watch # Watch mode
npm run test:vitest:coverage # With coverageHow it works:
- In Node.js: Uses real vitest (fast, native coverage)
- In UXP builds: Webpack redirects to adapter layer
- Same tests run in both environments!
| Method | Description |
|---|---|
assert(condition, message) |
Basic assertion |
assertEqual(actual, expected, message) |
Strict equality (===) |
assertNotEqual(actual, expected, message) |
Strict inequality (!==) |
assertTrue(value, message) |
Expects exactly true |
assertFalse(value, message) |
Expects exactly false |
assertNull(value, message) |
Expects null |
assertUndefined(value, message) |
Expects undefined |
assertThrows(fn, errorType, message) |
Expects function to throw |
assertDoesNotThrow(fn, message) |
Expects no error |
assertIncludes(array, value, message) |
Array contains value |
assertArrayEqual(actual, expected, message) |
Deep array equality |
assertObjectEqual(actual, expected, message) |
Deep object equality |
assertGreaterThan(actual, expected, message) |
Numeric comparison |
assertLessThan(actual, expected, message) |
Numeric comparison |
assertMatch(string, regex, message) |
Regex pattern match |
assertInstanceOf(obj, Constructor, message) |
Type checking |
┌─────────────────┐
│ Terminal CLI │ npm run test:uxp
└────────┬────────┘
│ Socket.IO (WebSocket)
▼
┌─────────────────┐
│ Proxy Server │ localhost:3001
└────────┬────────┘
│ Socket.IO (WebSocket)
▼
┌─────────────────┐
│ UXP Plugin │ Photoshop
└─────────────────┘
Why? UXP plugins cannot be Socket.IO servers (security constraint), so proxy routes commands between CLI and plugin.
uxp-test-runner/
├── src/
│ ├── core/
│ │ └── TestRunner.js # Pure JS test framework (460 lines)
│ ├── server/
│ │ └── test-proxy.js # Socket.IO proxy server (279 lines)
│ ├── plugin/
│ │ └── ws-client.js # Auto-reconnecting WebSocket client (242 lines)
│ ├── cli/
│ │ └── run-tests.js # Terminal CLI client (324 lines)
│ ├── tests/
│ │ ├── frameworkTests.js # Self-hosting tests (463 lines)
│ │ └── separationEngineTests.js
│ └── index.js # UXP entry point
├── test/
│ └── testrunner.test.js # Node.js Mocha unit tests (607 lines)
├── ARCHITECTURE.md # Complete code review & walkthrough
├── PHASE1-COMPLETE.md # Socket.IO bridge implementation notes
├── META-TESTING-COMPLETE.md # Dual testing approach documentation
└── package.json
| Command | Description |
|---|---|
npm test |
Run Node.js unit tests (default) |
npm run test:node |
Mocha unit tests (fast, 36ms) |
npm run test:uxp |
Run tests in Photoshop via WebSocket |
npm run test:all |
Run both Node.js and UXP tests |
npm run proxy |
Start Socket.IO proxy server |
npm run build |
Production build (no WebSocket client) |
npm run build:dev |
Development build (with WebSocket client) |
Run: npm test
Speed: ⚡ 36ms for 63 tests
Purpose:
- Test framework logic in isolation
- Fast feedback loop for TDD
- No Photoshop required
- CI/CD friendly
What's Tested:
- All 16 assertion methods
- Lifecycle hooks (beforeEach, afterEach, beforeAll, afterAll)
- Test organization (describe, skip, only)
- Result tracking
- Error handling
Run: Automatically included when tests execute in Photoshop
Purpose:
- Framework tests itself (meta!)
- Proves correctness in real UXP environment
- Dogfooding ensures API is usable
Result: 45 self-hosting tests verify framework behavior
Run: npm run test:uxp or manual trigger in Photoshop
Purpose:
- Test in real Photoshop environment
- Verify UXP API access
- End-to-end automation testing
Zero! Core framework has no dependencies.
express(^5.2.1) - HTTP server for proxysocket.io(^4.8.3) - WebSocket serversocket.io-client(^4.8.3) - WebSocket clientmocha(^11.7.5) - Node.js test runnerwebpack(^5.89.0) - Bundler
📖 README.md (this file)
📚 ARCHITECTURE.md - Complete walkthrough including:
- High-level architecture
- Component breakdown
- Data flow diagrams
- Key design decisions
- File-by-file explanation
- How to extend the framework
📋 PHASE1-COMPLETE.md - Socket.IO bridge implementation 🧪 META-TESTING-COMPLETE.md - Dual testing strategies
- Add method to
src/core/TestRunner.js - Add Node.js test to
test/testrunner.test.js - Add self-hosting test to
src/tests/frameworkTests.js - Run
npm testto verify
- Create file in
src/tests/ - Export registration function
- Import and call in
src/index.js
See ARCHITECTURE.md for detailed extension guide.
Check:
- Proxy running?
npm run proxy - Plugin loaded? Check UXP Developer Tool
- Console errors? Check Photoshop console
- Proxy status:
curl http://localhost:3001/status
Possible causes:
- Plugin not connected
- Proxy not routing messages
- Test actually hanging
Debug:
- Check proxy console for message routing
- Add
console.login test functions - Use
.only()to isolate failing test
Apache License 2.0
Relicensed from GPL-3.0 for explicit patent protection. See LICENSE file.
This project adapts patterns from:
- Bolt UXP (MIT) - WebSocket client patterns
- Adobe MCP (MIT) - Proxy server architecture
See NOTICE file for complete attributions.
Inspired by:
- Repository: https://github.com/electrosaur-labs/uxp-test-runner
- Issues: https://github.com/electrosaur-labs/uxp-test-runner/issues
- Code Review: ARCHITECTURE.md
Built with Claude Code