Documentation Index
Fetch the complete documentation index at: https://mintlify.com/rbbydotdev/opal/llms.txt
Use this file to discover all available pages before exploring further.
Testing Guide
Opal Editor uses two testing approaches: custom unit tests with the TestSuite framework and end-to-end tests with Playwright. This guide covers both testing methodologies.
Testing Frameworks
Unit Tests: TestSuite
Opal Editor uses a custom lightweight testing framework located at ~/workspace/source/src/lib/tests/TestSuite.ts:1-127. This framework provides:
- Simple, readable test syntax
- Async test support
- Rich assertion methods
- Clear console output
End-to-End Tests: Playwright
Playwright tests ensure the entire application works correctly in real browsers. Configuration is at ~/workspace/source/playwright.config.ts:1-52.
Running Tests
Run All E2E Tests
This runs Playwright tests across all configured browsers (Chromium, Firefox, WebKit).
Run Tests with UI
Opens the Playwright Test UI for interactive test debugging.
Run Tests in Headed Mode
Runs tests with visible browser windows.
Debug Tests
Runs tests in debug mode with Playwright Inspector.
Run Unit Tests
npm run test:unit
# or
npm run test:build
Runs the custom TestSuite unit tests (currently for build strategies).
Playwright Configuration
The Playwright config (~/workspace/source/playwright.config.ts:1-52) includes:
{
testDir: "./playwright-tests",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry"
}
}
Key settings:
- Test directory:
playwright-tests/
- Base URL:
http://localhost:3000
- Parallel execution: Enabled by default
- Retries: 2 retries on CI, 0 locally
- Trace collection: On first retry (for debugging)
- Browsers: Chromium, Firefox, WebKit
Web Server
Playwright automatically starts the dev server before running tests:
webServer: {
command: "npm run dev:slow",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI
}
Writing E2E Tests
Test File Location
Place E2E tests in the playwright-tests/ directory:
playwright-tests/
└── workspace-creation.spec.ts
Test Structure
Example from ~/workspace/source/playwright-tests/workspace-creation.spec.ts:1-112:
import { expect, test } from "@playwright/test";
test.describe("Workspace Creation Flow", () => {
test("should create a new workspace and add a markdown file", async ({ page }) => {
// Navigate to the home page
await page.goto("/");
// Wait for elements to be visible
await expect(page.getByRole("link", { name: "new workspace" })).toBeVisible();
// Interact with the page
await page.getByRole("link", { name: "new workspace" }).click();
// Assert URL changes
await expect(page).toHaveURL(/.*\/newWorkspace/);
// Verify UI elements
await expect(page.getByRole("dialog", { name: "New Workspace" })).toBeVisible();
});
});
Best Practices for E2E Tests
1. Use semantic selectors
// Good: Use role-based selectors
await page.getByRole("button", { name: "Create" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("my-workspace");
// Avoid: CSS selectors when possible
await page.locator(".create-button").click();
2. Wait for visibility
// Always wait for elements before interacting
await expect(page.getByRole("dialog")).toBeVisible();
await page.getByRole("button", { name: "Submit" }).click();
3. Use descriptive test names
test("should create workspace with custom name", async ({ page }) => {
// Clear test description of what's being tested
});
4. Test user flows, not implementation
// Test what users do
test("user can create and rename a file", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "New File" }).click();
// ... test the flow
});
Writing Unit Tests
Test File Location
Place unit tests next to the code they test with a .test.ts suffix:
src/
└── services/
└── build/
├── BuildStrategies.ts
└── BuildStrategies.test.ts
TestSuite API
The custom TestSuite framework (~/workspace/source/src/lib/tests/TestSuite.ts:1-127) provides:
Creating a test suite:
import { TestSuite } from "@/lib/tests/TestSuite";
const suite = new TestSuite("My Feature Tests");
Adding tests:
suite.test("should do something", async () => {
const result = await myFunction();
suite.assert(result === true, "Result should be true");
});
Assertion methods:
// Basic assertion
suite.assert(condition, "Error message");
// Equality
suite.assertEqual(actual, expected, "Values should match");
// Deep equality (JSON comparison)
suite.assertDeepEqual(actualObj, expectedObj, "Objects should match");
// Type checking
suite.assertType(value, "string", "Should be a string");
// Instance checking
suite.assertInstanceOf(obj, MyClass, "Should be MyClass instance");
// Exception testing
suite.assertThrows(() => {
throwingFunction();
}, "Expected error message");
// Async exception testing
await suite.assertThrowsAsync(async () => {
await asyncThrowingFunction();
}, "Expected error message");
Running the suite:
// At the end of your test file
if (import.meta.url === `file://${process.argv[1]}`) {
suite.run().catch(console.error);
}
export { suite as MyTestSuite };
Example Unit Test
From ~/workspace/source/src/services/build/BuildStrategies.test.ts:50-69:
import { TestSuite } from "@/lib/tests/TestSuite";
import { BuildDAO } from "@/data/dao/BuildDAO";
import { Disk } from "@/data/disk/Disk";
const suite = new TestSuite("Build Strategies");
suite.test("Freeform: should process markdown to HTML", async () => {
const sourceDisk = await createMockDisk({
"/index.md": "# Hello World\n\nThis is a test.",
"/global.css": "body { margin: 0; }",
});
const build = await createMockBuild("freeform", sourceDisk);
const runner = FreeformBuildRunner.Show({ build });
await runner.run();
const outputDisk = build.getOutputDisk();
const files = await outputDisk.listFiles(absPath("/"));
suite.assert(files.some((f) => f.endsWith("index.html")), "Should generate index.html");
suite.assert(files.some((f) => f.endsWith("global.css")), "Should copy global.css");
const htmlContent = await outputDisk.readFile(absPath("/index.html"));
suite.assert(htmlContent.includes("<h1>Hello World</h1>"), "Should convert markdown to HTML");
});
if (import.meta.url === `file://${process.argv[1]}`) {
suite.run().catch(console.error);
}
Unit Test Best Practices
1. Test one thing per test
// Good
suite.test("should convert markdown to HTML", async () => {
// Test only markdown conversion
});
suite.test("should copy static assets", async () => {
// Test only asset copying
});
// Avoid testing multiple unrelated behaviors in one test
2. Use descriptive test names
// Good
suite.test("should throw error when file does not exist", async () => {
// ...
});
// Avoid vague names
suite.test("test 1", async () => {
// ...
});
3. Create helper functions for setup
// Create reusable setup helpers
async function createMockDisk(files: Record<string, string>): Promise<Disk> {
const diskDAO = DiskDAO.CreateNew("memory");
await diskDAO.save();
const disk = await Disk.FromDAO(diskDAO);
for (const [path, content] of Object.entries(files)) {
await disk.writeFile(absPath(path), content);
}
return disk;
}
4. Use meaningful assertions
// Good: Specific error message
suite.assert(
htmlContent.includes("<h1>Hello World</h1>"),
"Should convert markdown heading to HTML h1 tag"
);
// Avoid: Generic messages
suite.assert(result === true, "Failed");
Test Coverage
Currently tested areas:
-
E2E Tests:
- Workspace creation flow
- File creation and management
- UI interactions
-
Unit Tests:
- Build strategies (Freeform, Eleventy)
- Template processing (Nunjucks, Liquid, Mustache, EJS)
- File system operations
- Event emitters
- Promise polyfills
Areas Needing Tests
Consider adding tests for:
- Git operations (clone, commit, push, pull)
- Publishing workflows (Netlify, Vercel, Cloudflare)
- Editor features (markdown editing, image upload)
- Search functionality
- Theme switching
- Import/export features
Debugging Tests
Playwright Debugging
1. Use Playwright Inspector
2. Add debugging statements
test("debug example", async ({ page }) => {
await page.pause(); // Pauses execution
console.log(await page.textContent("h1")); // Log content
});
3. View traces
After a test failure, open the HTML report:
npx playwright show-report
Unit Test Debugging
1. Add console logs
suite.test("debug example", async () => {
const result = await myFunction();
console.log("Result:", result);
suite.assertEqual(result, expected);
});
2. Run single test file
tsx src/path/to/your-test.test.ts
Continuous Integration
On CI environments:
- Tests retry up to 2 times on failure
- Tests run sequentially (not in parallel)
- Dev server must start fresh (no reuse)
- HTML reports are generated for debugging
Test Organization
File Naming
- E2E tests:
*.spec.ts in playwright-tests/
- Unit tests:
*.test.ts next to source files
Test Grouping
// E2E: Use test.describe for grouping
test.describe("Workspace Features", () => {
test("should create workspace", async ({ page }) => {});
test("should delete workspace", async ({ page }) => {});
});
// Unit: Create separate suite instances or group with comments
const suite = new TestSuite("Feature Group");
// Group 1: Basic functionality
suite.test("test 1", async () => {});
suite.test("test 2", async () => {});
// Group 2: Edge cases
suite.test("test 3", async () => {});
Contributing Tests
When contributing code:
- Add E2E tests for new user-facing features
- Add unit tests for complex business logic
- Ensure all tests pass before submitting PR
- Update this guide if introducing new testing patterns
See the Contributing Guide for more information.
Resources