This document provides comprehensive guidance on testing the Quarto Review extension, including how to run tests, add new test cases, and understand the test infrastructure.
The Quarto Review extension has a comprehensive test suite covering:
tests/
├── fixtures/ # Test fixtures (see fixtures/README.md)
│ ├── transformation/ # Text transformation test cases
│ │ ├── inputs/ # Original markdown
│ │ ├── edits/ # Edited markdown
│ │ └── expected/ # Expected outputs
│ │ ├── critic-markup/ # Expected CriticMarkup
│ │ ├── accepted/ # Expected after accepting
│ │ └── rejected/ # Expected after rejecting
│ ├── rendering/ # Markdown rendering tests
│ ├── documents/ # Complete document fixtures
│ └── operations/ # Operation sequence tests
│
├── unit/ # Unit tests
│ └── core/
│ ├── transformation-pipeline.test.ts
│ └── markdown-rendering.test.ts
│
├── integration/ # Integration tests
│ └── transformation-pipeline-integration.test.ts
│
├── e2e/ # E2E tests
│ └── text-transformation.spec.ts
│
└── utils/ # Test utilities
└── fixture-loader.ts # Fixture loading utility
# Run all tests (unit + integration + E2E)
npm test
# Run with coverage
npm run test:coverage
# Run all unit tests
npm run test:unit
# Run specific test file
npm run test:unit transformation-pipeline
# Run in watch mode
npm run test:unit -- --watch
# Run all integration tests
npm run test:integration
# Run specific integration test
npm run test:integration transformation-pipeline-integration
# Run all E2E tests
npm run test:e2e
# Run E2E tests in headed mode (see browser)
npm run test:e2e -- --headed
# Run specific E2E test
npm run test:e2e text-transformation
# Debug E2E tests
npm run test:e2e -- --debug
# Run unit tests in watch mode (auto-rerun on file changes)
npm run test:unit -- --watch
# Run specific test file in watch mode
npm run test:unit transformation-pipeline -- --watch
# Generate coverage report
npm run test:coverage
# View coverage report in browser
open coverage/index.html
The Quarto Review test suite is designed to make adding new test cases easy. There are two primary methods:
Fixture-based tests are ideal for testing text transformations because they:
Create input file in tests/fixtures/transformation/inputs/
# File: tests/fixtures/transformation/inputs/my-test-case.md
Original content here
Create edit file in tests/fixtures/transformation/edits/
# File: tests/fixtures/transformation/edits/my-test-case.md
Modified content here
Create expected CriticMarkup in tests/fixtures/transformation/expected/critic-markup/
# File: tests/fixtures/transformation/expected/critic-markup/my-test-case.md
{~~Original~>Modified~~} content here
That’s it! The test suite will automatically:
# 1. Create input
cat > tests/fixtures/transformation/inputs/list-preserve-markers.md << 'EOF'
- Item 1
- Item 2
- Item 3
EOF
# 2. Create edit (modify second item)
cat > tests/fixtures/transformation/edits/list-preserve-markers.md << 'EOF'
- Item 1
- Item 2 modified
- Item 3
EOF
# 3. Create expected CriticMarkup
cat > tests/fixtures/transformation/expected/critic-markup/list-preserve-markers.md << 'EOF'
- Item 1
- Item 2{++ modified++}
- Item 3
EOF
# Run the test
npm run test:unit transformation-pipeline
For more complex scenarios that require setup or assertions, add tests directly in test files.
// File: tests/unit/core/transformation-pipeline.test.ts
it('should handle my specific edge case', () => {
const original = 'some original content';
const edited = 'some edited content';
const changes = generateChanges(original, edited);
const criticMarkup = changesToCriticMarkup(original, changes);
const accepted = stripCriticMarkup(criticMarkup, true);
expect(accepted).toBe(edited);
expect(criticMarkup).toContain('{++');
});
// File: tests/integration/transformation-pipeline-integration.test.ts
it('should handle complex workflow', () => {
// Create test scenario
const original = 'original';
const edit1 = 'edit 1';
const edit2 = 'edit 2';
// Process through pipeline
const changes1 = generateChanges(original, edit1);
const critic1 = changesToCriticMarkup(original, changes1);
const accepted1 = stripCriticMarkup(critic1, true);
const changes2 = generateChanges(accepted1, edit2);
const critic2 = changesToCriticMarkup(accepted1, changes2);
const accepted2 = stripCriticMarkup(critic2, true);
// Verify final state
expect(accepted2).toBe(edit2);
});
// File: tests/e2e/text-transformation.spec.ts
test('should handle my browser scenario', async ({ page }) => {
await createTestDocument(page, [
{ markdown: 'Test content' },
]);
await page.waitForSelector('[data-review-id]');
await page.click('[data-review-id="test.para-1"]');
await page.waitForSelector('.milkdown-editor');
await page.keyboard.type('Modified content');
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
const result = await page.evaluate(() => {
const quartoReview = (window as any).quartoReview;
return quartoReview?.changes?.toCleanMarkdown();
});
expect(result).toContain('Modified content');
});
Located in: tests/unit/core/transformation-pipeline.test.ts
What it tests:
generateChanges() - Diff generationchangesToCriticMarkup() - CriticMarkup conversionstripCriticMarkup() - Accept/reject changesAdd test cases by: Creating fixtures in tests/fixtures/transformation/
Located in: tests/unit/core/markdown-rendering.test.ts
What it tests:
Add test cases by: Adding fixtures in tests/fixtures/rendering/
Located in: tests/integration/transformation-pipeline-integration.test.ts
What it tests:
Add test cases by: Adding test functions to the integration test file
Located in: tests/e2e/text-transformation.spec.ts
What it tests:
Add test cases by: Adding Playwright test functions
# Run single test file with verbose output
npm run test:unit transformation-pipeline -- --reporter=verbose
# Run single test by name
npm run test:unit -- -t "should handle list deletion"
# Debug with Node inspector
node --inspect-brk node_modules/.bin/vitest run transformation-pipeline
# Run in headed mode (see browser)
npm run test:e2e -- --headed
# Run in debug mode (pauses for inspection)
npm run test:e2e -- --debug
# Run specific test
npm run test:e2e -- -g "should edit a paragraph"
# Slow down execution
npm run test:e2e -- --headed --slow-mo=1000
When a test fails:
-t to run just that testdebugger;If behavior changes and you need to update expected outputs:
# For fixture-based tests: manually update the expected files
vim tests/fixtures/transformation/expected/critic-markup/my-test.md
# For snapshot tests: update snapshots
npm run test:unit -- -u
⚠️ Warning: Only update expected outputs if you’re confident the new behavior is correct!
The test suite runs automatically on:
Located in: .github/workflows/test.yml
The CI pipeline:
Defined in vitest.config.ts:
coverage: {
lines: 60,
functions: 60,
branches: 50,
}
should preserve list markers when editing list itemsTest files:
*.test.ts*.spec.tsFixture files:
list-delete-item.mdtable-edit-cell-with-pipes.mdunicode-emoji-edit.mddescribe('Feature Name', () => {
describe('Specific Behavior', () => {
it('should do expected thing', () => {
// Arrange
const input = 'test';
// Act
const result = doSomething(input);
// Assert
expect(result).toBe('expected');
});
});
});
If tests are slow:
test.describe.configure({ mode: 'parallel' })// Wrong
await page.click('button');
const result = await page.textContent('div');
// Right
await page.click('button');
await page.waitForTimeout(500); // or use waitForSelector
const result = await page.textContent('div');
// Wrong
expect(result).toBe(expected);
// Right
expect(result.trim()).toBe(expected.trim());
// Wrong
await page.waitForTimeout(1000); // Too slow
// Right
await page.waitForSelector('[data-loaded]'); // Event-driven
beforeEach(() => {
// Reset state
});
afterEach(() => {
// Clean up
});
Issue: Tests pass locally but fail in CI
Issue: E2E tests are flaky
waitForSelector instead of waitForTimeout, increase timeoutsIssue: Coverage is lower than expected
npm run test:coverage and check coverage/index.htmlIssue: Fixture-based tests not running
tests/fixtures/README.mdThe FixtureLoader class provides utilities for loading test fixtures:
import { fixtureLoader } from '../utils/fixture-loader';
// Load a text file
const content = fixtureLoader.loadText('transformation/inputs/test.md');
// Load a JSON file
const data = fixtureLoader.loadJSON('operations/scenarios/test.json');
// Check if file exists
if (fixtureLoader.exists('transformation/inputs/test.md')) {
// ...
}
// Get all transformation test cases
const testCases = fixtureLoader.getTransformationTestCases();
describe('My Feature', () => {
beforeEach(() => {
// Setup
});
afterEach(() => {
// Cleanup
});
it('should do expected behavior', () => {
// Arrange
const input = 'test input';
// Act
const result = myFunction(input);
// Assert
expect(result).toBe('expected output');
});
describe('Edge Cases', () => {
it('should handle empty input', () => {
const result = myFunction('');
expect(result).toBe('');
});
});
});
| Command | Description |
|---|---|
npm test |
Run all tests |
npm run test:unit |
Run unit tests |
npm run test:integration |
Run integration tests |
npm run test:e2e |
Run E2E tests |
npm run test:coverage |
Run with coverage |
npm run test:unit -- --watch |
Watch mode |
npm run test:e2e -- --headed |
E2E in browser |
npm run test:e2e -- --debug |
E2E debug mode |
For more information, see: