This commit is contained in:
Skyler Lehmkuhl 2025-10-20 00:44:47 -04:00
parent 97b9ff71b7
commit 5a72743209
9 changed files with 1225 additions and 0 deletions

147
tests/README.md Normal file
View File

@ -0,0 +1,147 @@
# Lightningbeam UI Tests
Automated UI tests for Lightningbeam using WebdriverIO and tauri-driver.
## Prerequisites
1. **Install test dependencies**:
```bash
pnpm add -D @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/spec-reporter @wdio/globals
```
**Important**: If the `@wdio/local-runner` package hangs during installation, you must install it in your native OS environment (not in a container). The pnpm store can have conflicts when switching between different OS contexts. If you originally ran `pnpm install` on your host system, install the test dependencies there as well.
2. **Build the application** - Tests require the release build:
```bash
pnpm tauri build
```
**Note**: The debug build (`pnpm tauri build --debug`) won't work for tests because it expects a dev server to be running. Tests use the self-contained release build.
3. **Install tauri-driver** - Download from [tauri-apps/tauri releases](https://github.com/tauri-apps/tauri/releases):
```bash
# Linux example
cargo install tauri-driver
# Or download binary and add to PATH
```
## Running Tests
### 1. Start tauri-driver
In a separate terminal, start tauri-driver:
```bash
tauri-driver --port 4444
```
### 2. Run all tests
```bash
pnpm test
```
### Run tests in watch mode
```bash
pnpm test:watch
```
### Run specific test file
```bash
pnpm wdio run wdio.conf.js --spec tests/specs/shapes.test.js
```
## Test Structure
```
tests/
├── helpers/
│ ├── app.js # App lifecycle helpers
│ ├── canvas.js # Canvas interaction utilities
│ └── assertions.js # Custom assertions
├── specs/
│ ├── shapes.test.js # Shape drawing tests
│ ├── grouping.test.js # Shape grouping tests
│ └── paint-bucket.test.js # Paint bucket tool tests
└── fixtures/ # Test data files
```
## Writing Tests
### Example: Drawing a Rectangle
```javascript
import { drawRectangle } from '../helpers/canvas.js';
import { assertShapeExists } from '../helpers/assertions.js';
it('should draw a rectangle', async () => {
await drawRectangle(100, 100, 200, 150);
await assertShapeExists(200, 175, 'Rectangle should exist at center');
});
```
### Available Helpers
#### Canvas Helpers (`canvas.js`)
- `clickCanvas(x, y)` - Click at coordinates
- `dragCanvas(fromX, fromY, toX, toY)` - Drag operation
- `drawRectangle(x, y, width, height)` - Draw rectangle
- `drawEllipse(x, y, width, height)` - Draw ellipse
- `selectTool(toolName)` - Select a tool by name
- `selectMultipleShapes(points)` - Select multiple shapes with Ctrl
- `useKeyboardShortcut(key, withCtrl)` - Use keyboard shortcuts
- `getPixelColor(x, y)` - Get color at coordinates
- `hasShapeAt(x, y)` - Check if shape exists at point
#### Assertion Helpers (`assertions.js`)
- `assertShapeExists(x, y, message)` - Assert shape at coordinates
- `assertNoShapeAt(x, y, message)` - Assert no shape at coordinates
- `assertPixelColor(x, y, color, message)` - Assert pixel color
- `assertColorApproximately(color1, color2, tolerance)` - Fuzzy color match
## Adding Data Attributes for Testing
To make UI elements easier to test, add `data-tool` attributes to tool buttons in the UI:
```javascript
// Example in main.js
const rectangleTool = document.createElement('button');
rectangleTool.setAttribute('data-tool', 'rectangle');
```
Current expected data attributes:
- `data-tool="rectangle"` - Rectangle tool button
- `data-tool="ellipse"` - Ellipse tool button
- `data-tool="dropper"` - Paint bucket/dropper tool button
- Add more as needed...
## Platform Support
- **Linux**: Full support with webkit2gtk
- **Windows**: Full support with WebView2
- **macOS**: Limited support (no WKWebView driver available)
## Troubleshooting
### Tests fail to start
- Ensure the release build exists: `./src-tauri/target/release/lightningbeam`
- Check that `tauri-driver` is in your PATH
### Canvas interactions don't work
- Verify that tool buttons have `data-tool` attributes
- Check that canvas element is present with `document.querySelector('canvas')`
### Screenshots directory missing
```bash
mkdir -p tests/screenshots
```
## CI Integration
See `.github/workflows/` for example GitHub Actions configuration (to be added).
## Future Enhancements
- [ ] Add color picker test helpers
- [ ] Add timeline/keyframe test helpers
- [ ] Add layer management test helpers
- [ ] Visual regression testing with screenshot comparison
- [ ] Performance benchmarks
- [ ] Add Tauri commands for better state inspection

52
tests/helpers/app.js Normal file
View File

@ -0,0 +1,52 @@
/**
* App helper utilities for Tauri application testing
*/
/**
* Wait for the Lightningbeam app to be fully loaded and ready
* @param {number} timeout - Maximum time to wait in ms
*/
export async function waitForAppReady(timeout = 5000) {
await browser.waitForApp();
// Check for "Create New File" dialog and click Create if present
const createButton = await browser.$('button*=Create');
if (await createButton.isExisting()) {
await createButton.click();
await browser.pause(500); // Wait for dialog to close
}
// Wait for the main canvas to be present
const canvas = await browser.$('canvas');
await canvas.waitForExist({ timeout });
// Additional wait for any initialization
await browser.pause(500);
}
/**
* Get the main canvas element
* @returns {Promise<WebdriverIO.Element>}
*/
export async function getCanvas() {
return await browser.$('canvas');
}
/**
* Get canvas dimensions
* @returns {Promise<{width: number, height: number}>}
*/
export async function getCanvasSize() {
const canvas = await getCanvas();
const size = await canvas.getSize();
return size;
}
/**
* Take a screenshot of the canvas
* @param {string} filename - Name for the screenshot file
*/
export async function takeCanvasScreenshot(filename) {
const canvas = await getCanvas();
return await canvas.saveScreenshot(`./tests/screenshots/${filename}`);
}

View File

@ -0,0 +1,75 @@
/**
* Custom assertion helpers for Lightningbeam UI tests
*/
import { getPixelColor, hasShapeAt } from './canvas.js';
import { expect } from '@wdio/globals';
/**
* Assert that a shape exists at the given coordinates
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {string} message - Custom error message
*/
export async function assertShapeExists(x, y, message = 'Expected shape to exist at coordinates') {
const shapeExists = await hasShapeAt(x, y);
expect(shapeExists).toBe(true, `${message} (${x}, ${y})`);
}
/**
* Assert that no shape exists at the given coordinates
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {string} message - Custom error message
*/
export async function assertNoShapeAt(x, y, message = 'Expected no shape at coordinates') {
const shapeExists = await hasShapeAt(x, y);
expect(shapeExists).toBe(false, `${message} (${x}, ${y})`);
}
/**
* Assert that a pixel has a specific color
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {string} expectedColor - Expected color in hex format
* @param {string} message - Custom error message
*/
export async function assertPixelColor(x, y, expectedColor, message = 'Expected pixel color to match') {
const actualColor = await getPixelColor(x, y);
expect(actualColor.toLowerCase()).toBe(expectedColor.toLowerCase(),
`${message}. Expected ${expectedColor}, got ${actualColor} at (${x}, ${y})`);
}
/**
* Assert that a color is approximately equal to another (with tolerance)
* Useful for anti-aliasing and rendering differences
* @param {string} color1 - First color in hex format
* @param {string} color2 - Second color in hex format
* @param {number} tolerance - Tolerance per channel (0-255)
*/
export function assertColorApproximately(color1, color2, tolerance = 10) {
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
const rDiff = Math.abs(rgb1.r - rgb2.r);
const gDiff = Math.abs(rgb1.g - rgb2.g);
const bDiff = Math.abs(rgb1.b - rgb2.b);
expect(rDiff).toBeLessThanOrEqual(tolerance, `Red channel difference ${rDiff} exceeds tolerance ${tolerance}`);
expect(gDiff).toBeLessThanOrEqual(tolerance, `Green channel difference ${gDiff} exceeds tolerance ${tolerance}`);
expect(bDiff).toBeLessThanOrEqual(tolerance, `Blue channel difference ${bDiff} exceeds tolerance ${tolerance}`);
}
/**
* Convert hex color to RGB object
* @param {string} hex - Hex color string
* @returns {{r: number, g: number, b: number}}
*/
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}

224
tests/helpers/canvas.js Normal file
View File

@ -0,0 +1,224 @@
/**
* Canvas interaction utilities for UI testing
*/
/**
* Click at specific coordinates on the canvas
* @param {number} x - X coordinate relative to canvas
* @param {number} y - Y coordinate relative to canvas
*/
export async function clickCanvas(x, y) {
await browser.clickCanvas(x, y);
await browser.pause(100); // Wait for render
}
/**
* Drag from one point to another on the canvas
* @param {number} fromX - Starting X coordinate
* @param {number} fromY - Starting Y coordinate
* @param {number} toX - Ending X coordinate
* @param {number} toY - Ending Y coordinate
*/
export async function dragCanvas(fromX, fromY, toX, toY) {
await browser.dragCanvas(fromX, fromY, toX, toY);
await browser.pause(200); // Wait for render
}
/**
* Draw a rectangle on the canvas
* @param {number} x - Top-left X coordinate
* @param {number} y - Top-left Y coordinate
* @param {number} width - Rectangle width
* @param {number} height - Rectangle height
* @param {boolean} filled - Whether to fill the shape (default: true)
*/
export async function drawRectangle(x, y, width, height, filled = true) {
// Select the rectangle tool
await selectTool('rectangle');
// Set fill option
await browser.execute((filled) => {
if (window.context) {
window.context.fillShape = filled;
}
}, filled);
// Draw by dragging from start to end point
await dragCanvas(x, y, x + width, y + height);
// Wait for shape to be created
await browser.pause(300);
}
/**
* Draw an ellipse on the canvas
* @param {number} x - Top-left X coordinate
* @param {number} y - Top-left Y coordinate
* @param {number} width - Ellipse width
* @param {number} height - Ellipse height
* @param {boolean} filled - Whether to fill the shape (default: true)
*/
export async function drawEllipse(x, y, width, height, filled = true) {
// Select the ellipse tool
await selectTool('ellipse');
// Set fill option
await browser.execute((filled) => {
if (window.context) {
window.context.fillShape = filled;
}
}, filled);
// Draw by dragging from start to end point
await dragCanvas(x, y, x + width, y + height);
// Wait for shape to be created
await browser.pause(300);
}
/**
* Select a tool from the toolbar
* @param {string} toolName - Name of the tool ('rectangle', 'ellipse', 'brush', etc.)
*/
export async function selectTool(toolName) {
const toolButton = await browser.$(`[data-tool="${toolName}"]`);
await toolButton.click();
await browser.pause(100);
}
/**
* Select multiple shapes by dragging a selection box over them
* @param {Array<{x: number, y: number}>} points - Array of points representing shapes to select
*/
export async function selectMultipleShapes(points) {
// First, make sure we're in select mode
await selectTool('select');
// Calculate bounding box that encompasses all points
const minX = Math.min(...points.map(p => p.x)) - 10;
const minY = Math.min(...points.map(p => p.y)) - 10;
const maxX = Math.max(...points.map(p => p.x)) + 10;
const maxY = Math.max(...points.map(p => p.y)) + 10;
// Drag a selection box from top-left to bottom-right
await dragCanvas(minX, minY, maxX, maxY);
await browser.pause(200);
}
/**
* Use keyboard shortcut / menu action
* Since Tauri menu shortcuts don't reach the browser, we invoke actions directly
* @param {string} key - Key to press (e.g., 'g' for group)
* @param {boolean} withCtrl - Whether to hold Ctrl (ignored, kept for compatibility)
*/
export async function useKeyboardShortcut(key, withCtrl = true) {
if (key === 'g') {
// Call group action directly without serializing the whole function
await browser.execute('window.actions.group.create()');
}
await browser.pause(300); // Give time for the action to process
}
/**
* Get pixel color at specific coordinates (requires canvas access)
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @returns {Promise<string>} Color in hex format
*/
export async function getPixelColor(x, y) {
const result = await browser.execute(function(x, y) {
const canvas = document.querySelector('canvas.stage');
if (!canvas) return null;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(x, y, 1, 1);
const data = imageData.data;
// Convert to hex
const r = data[0].toString(16).padStart(2, '0');
const g = data[1].toString(16).padStart(2, '0');
const b = data[2].toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}, x, y);
return result;
}
/**
* Check if a shape exists at given coordinates by checking if pixel is not background
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {string} backgroundColor - Expected background color (default white)
* @returns {Promise<boolean>}
*/
export async function hasShapeAt(x, y, backgroundColor = '#ffffff') {
const color = await getPixelColor(x, y);
return color && color.toLowerCase() !== backgroundColor.toLowerCase();
}
/**
* Double-click at specific coordinates on the canvas (to enter group editing mode)
* @param {number} x - X coordinate relative to canvas
* @param {number} y - Y coordinate relative to canvas
*/
export async function doubleClickCanvas(x, y) {
const canvas = await browser.$('canvas.stage');
const location = await canvas.getLocation();
// Perform double-click using performActions
await browser.performActions([{
type: 'pointer',
id: 'mouse',
parameters: { pointerType: 'mouse' },
actions: [
{ type: 'pointerMove', duration: 0, x: location.x + x, y: location.y + y },
{ type: 'pointerDown', button: 0 },
{ type: 'pointerUp', button: 0 },
{ type: 'pause', duration: 50 },
{ type: 'pointerDown', button: 0 },
{ type: 'pointerUp', button: 0 }
]
}]);
await browser.pause(300); // Wait for group to be entered
}
/**
* Set the timeline playhead to a specific time
* @param {number} time - Time in seconds
*/
export async function setPlayheadTime(time) {
await browser.execute(function(timeValue) {
if (window.context && window.context.activeObject) {
window.context.activeObject.currentTime = timeValue;
// Update timeline widget if it exists
if (window.context.timelineWidget && window.context.timelineWidget.timelineState) {
window.context.timelineWidget.timelineState.currentTime = timeValue;
}
}
}, time);
await browser.pause(100);
}
/**
* Get the current playhead time
* @returns {Promise<number>} Current time in seconds
*/
export async function getPlayheadTime() {
return await browser.execute(function() {
if (window.context && window.context.activeObject) {
return window.context.activeObject.currentTime;
}
return 0;
});
}
/**
* Add a keyframe at the current playhead position for selected shapes/objects
*/
export async function addKeyframe() {
await browser.execute('window.addKeyframeAtPlayhead && window.addKeyframeAtPlayhead()');
await browser.pause(200);
}

View File

@ -0,0 +1,132 @@
/**
* Group editing tests for Lightningbeam
* Tests that shapes can be edited inside groups with correct relative positioning
*/
import { describe, it, before } from 'mocha';
import { expect } from '@wdio/globals';
import { waitForAppReady } from '../helpers/app.js';
import {
drawRectangle,
selectMultipleShapes,
useKeyboardShortcut,
doubleClickCanvas,
clickCanvas
} from '../helpers/canvas.js';
import { assertShapeExists } from '../helpers/assertions.js';
describe('Group Editing', () => {
before(async () => {
await waitForAppReady();
});
describe('Entering and Editing Groups', () => {
it('should maintain shape positions when editing inside a group', async () => {
// Draw a rectangle
await drawRectangle(200, 200, 100, 100);
// Verify it exists at the expected location
await assertShapeExists(250, 250, 'Rectangle should exist at center before grouping');
// Select it (click on the center)
await clickCanvas(250, 250);
await browser.pause(200);
// Group it (even though it's just one shape)
await useKeyboardShortcut('g', true);
await browser.pause(300);
// The shape should still be visible at the same location
await assertShapeExists(250, 250, 'Rectangle should still exist at same position after grouping');
// Double-click on the group to enter editing mode
await doubleClickCanvas(250, 250);
// The shape should STILL be at the same position when editing the group
await assertShapeExists(250, 250, 'Rectangle should remain at same position when editing group');
});
it('should correctly position new shapes drawn inside a group', async () => {
// Draw a rectangle
await drawRectangle(100, 400, 80, 80);
// Select and group it
await clickCanvas(140, 440);
await browser.pause(200);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Double-click to enter group editing mode
await doubleClickCanvas(140, 440);
// Draw another rectangle inside the group at a specific location
await drawRectangle(200, 400, 80, 80);
// Verify the new shape is where we drew it
await assertShapeExists(240, 440, 'New shape should be at the coordinates where it was drawn');
// Verify the original shape still exists
await assertShapeExists(140, 440, 'Original shape should still exist');
});
it('should handle nested group editing with correct positioning', async () => {
// Create first group with two shapes
await drawRectangle(400, 100, 60, 60);
await drawRectangle(480, 100, 60, 60);
await selectMultipleShapes([
{ x: 430, y: 130 },
{ x: 510, y: 130 }
]);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Verify both shapes exist
await assertShapeExists(430, 130, 'First shape should exist');
await assertShapeExists(510, 130, 'Second shape should exist');
// Create another shape and group everything together
await drawRectangle(400, 180, 60, 60);
await selectMultipleShapes([
{ x: 470, y: 130 }, // Center of first group
{ x: 430, y: 210 } // Center of new shape
]);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Double-click to enter outer group
await doubleClickCanvas(470, 130);
// Double-click again to enter inner group
await doubleClickCanvas(470, 130);
// All shapes should still be at their original positions
await assertShapeExists(430, 130, 'First shape should maintain position in nested group');
await assertShapeExists(510, 130, 'Second shape should maintain position in nested group');
});
});
describe('Mouse Coordinate Transformation', () => {
it('should correctly translate mouse coordinates when drawing in groups', async () => {
// Draw and group a shape
await drawRectangle(300, 300, 50, 50);
await clickCanvas(325, 325);
await browser.pause(200);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Enter group
await doubleClickCanvas(325, 325);
// Draw shapes at precise coordinates to test mouse transformation
await drawRectangle(350, 300, 30, 30);
await drawRectangle(300, 350, 30, 30);
await drawRectangle(350, 350, 30, 30);
// Verify all shapes are at expected positions
await assertShapeExists(365, 315, 'Shape to the right should be at correct position');
await assertShapeExists(315, 365, 'Shape below should be at correct position');
await assertShapeExists(365, 365, 'Shape diagonal should be at correct position');
await assertShapeExists(325, 325, 'Original shape should still exist');
});
});
});

View File

@ -0,0 +1,134 @@
/**
* Shape grouping tests for Lightningbeam
*/
import { describe, it, before, beforeEach } from 'mocha';
import { expect } from '@wdio/globals';
import { waitForAppReady } from '../helpers/app.js';
import { drawRectangle, drawEllipse, selectMultipleShapes, useKeyboardShortcut, clickCanvas } from '../helpers/canvas.js';
import { assertShapeExists } from '../helpers/assertions.js';
describe('Shape Grouping', () => {
before(async () => {
await waitForAppReady();
});
describe('Grouping Multiple Shapes', () => {
it('should group two rectangles together', async () => {
// Draw two rectangles
await drawRectangle(100, 100, 100, 100);
await drawRectangle(250, 100, 100, 100);
// Select both shapes (click centers with Ctrl held)
await selectMultipleShapes([
{ x: 150, y: 150 },
{ x: 300, y: 150 }
]);
// Group with Ctrl+G
await useKeyboardShortcut('g', true);
// Verify both shapes still exist after grouping
await assertShapeExists(150, 150, 'First rectangle should still exist after grouping');
await assertShapeExists(300, 150, 'Second rectangle should still exist after grouping');
});
it('should group rectangle and ellipse together', async () => {
// Draw rectangle and ellipse
await drawRectangle(100, 250, 120, 80);
await drawEllipse(280, 250, 120, 80);
// Select both shapes
await selectMultipleShapes([
{ x: 160, y: 290 },
{ x: 340, y: 290 }
]);
// Group them
await useKeyboardShortcut('g', true);
// Verify shapes exist
await assertShapeExists(160, 290, 'Rectangle should exist in group');
await assertShapeExists(340, 290, 'Ellipse should exist in group');
});
it('should group three or more shapes', async () => {
// Draw three rectangles
await drawRectangle(50, 400, 80, 80);
await drawRectangle(150, 400, 80, 80);
await drawRectangle(250, 400, 80, 80);
// Select all three
await selectMultipleShapes([
{ x: 90, y: 440 },
{ x: 190, y: 440 },
{ x: 290, y: 440 }
]);
// Group them
await useKeyboardShortcut('g', true);
// Verify all shapes exist
await assertShapeExists(90, 440, 'First shape should exist in group');
await assertShapeExists(190, 440, 'Second shape should exist in group');
await assertShapeExists(290, 440, 'Third shape should exist in group');
});
});
describe('Group Manipulation', () => {
it('should be able to select and move a group', async () => {
// Draw two shapes
await drawRectangle(400, 100, 80, 80);
await drawRectangle(500, 100, 80, 80);
// Select and group
await selectMultipleShapes([
{ x: 440, y: 140 },
{ x: 540, y: 140 }
]);
await useKeyboardShortcut('g', true);
// Click on the group to select it (click between the shapes)
await clickCanvas(490, 140);
// Note: Moving would require drag testing which is already covered in canvas.js
// This test verifies the group can be selected
});
});
describe('Nested Groups', () => {
it('should allow grouping of groups', async () => {
// Create first group
await drawRectangle(100, 100, 60, 60);
await drawRectangle(180, 100, 60, 60);
await selectMultipleShapes([
{ x: 130, y: 130 },
{ x: 210, y: 130 }
]);
await useKeyboardShortcut('g', true);
// Create second group
await drawRectangle(100, 200, 60, 60);
await drawRectangle(180, 200, 60, 60);
await selectMultipleShapes([
{ x: 130, y: 230 },
{ x: 210, y: 230 }
]);
await useKeyboardShortcut('g', true);
// Now group both groups together
// Select center of each group
await selectMultipleShapes([
{ x: 170, y: 130 },
{ x: 170, y: 230 }
]);
await useKeyboardShortcut('g', true);
// Verify all original shapes still exist
await assertShapeExists(130, 130, 'First group first shape should exist');
await assertShapeExists(210, 130, 'First group second shape should exist');
await assertShapeExists(130, 230, 'Second group first shape should exist');
await assertShapeExists(210, 230, 'Second group second shape should exist');
});
});
});

View File

@ -0,0 +1,88 @@
/**
* Paint bucket tool tests for Lightningbeam
*/
import { describe, it, before } from 'mocha';
import { expect } from '@wdio/globals';
import { waitForAppReady } from '../helpers/app.js';
import { drawRectangle, selectTool, clickCanvas, getPixelColor } from '../helpers/canvas.js';
import { assertPixelColor } from '../helpers/assertions.js';
describe('Paint Bucket Tool', () => {
before(async () => {
await waitForAppReady();
});
describe('Fill Shape', () => {
it('should fill a rectangle with color', async () => {
// Draw an unfilled rectangle (outline only)
await drawRectangle(100, 100, 200, 150, false);
// Get the color before filling (should be stroke/outline only, center is white)
const colorBefore = await getPixelColor(200, 175);
// Select paint bucket tool
await selectTool('paint_bucket');
// Click inside the rectangle to fill it
await clickCanvas(200, 175);
// Get the color after filling
const colorAfter = await getPixelColor(200, 175);
// The color should have changed from white background to filled color
expect(colorBefore.toLowerCase()).toBe('#ffffff'); // Was white (unfilled)
expect(colorAfter.toLowerCase()).not.toBe('#ffffff'); // Now filled with a color
});
it('should fill only the clicked shape, not adjacent shapes', async () => {
// Draw two separate unfilled rectangles
await drawRectangle(100, 300, 100, 100, false);
await drawRectangle(250, 300, 100, 100, false);
// Fill only the first rectangle
await selectTool('paint_bucket');
await clickCanvas(150, 350);
// Get colors from both shapes
const firstColor = await getPixelColor(150, 350);
const secondColor = await getPixelColor(300, 350);
// The shapes should potentially have different colors
// (or at least we confirmed we could click them individually)
});
});
describe('Fill with Different Colors', () => {
it('should respect the selected fill color when using paint bucket', async () => {
// This test would require setting a specific fill color first
// Then drawing and filling a shape
// For now, this is a placeholder structure
await drawRectangle(400, 100, 150, 100, false);
// TODO: Add color selection logic when color picker helpers are available
// await selectColor('#ff0000');
await selectTool('paint_bucket');
await clickCanvas(475, 150);
// Verify the fill color
const color = await getPixelColor(475, 150);
// TODO: Assert expected color when we know how to set it
});
});
describe('Fill Gaps Setting', () => {
it('should handle fill gaps setting for incomplete shapes', async () => {
// This test would draw an incomplete shape and test the fillGaps parameter
// Placeholder for now - would need specific incomplete shape drawing
// The fillGaps setting controls how the paint bucket handles gaps in shapes
// This is a more advanced test that would need:
// 1. A way to draw incomplete/open shapes
// 2. A way to set the fillGaps parameter
// 3. Verification that the fill respects the gap threshold
});
});
});

100
tests/specs/shapes.test.js Normal file
View File

@ -0,0 +1,100 @@
/**
* Shape drawing tests for Lightningbeam
*/
import { describe, it, before } from 'mocha';
import { waitForAppReady } from '../helpers/app.js';
import { drawRectangle, drawEllipse } from '../helpers/canvas.js';
import { assertShapeExists } from '../helpers/assertions.js';
describe('Shape Drawing', () => {
before(async () => {
await waitForAppReady();
});
describe('Rectangle Tool', () => {
it('should draw a rectangle on the canvas', async () => {
// Draw a rectangle at (100, 100) with size 200x150
await drawRectangle(100, 100, 200, 150);
// Verify the shape exists by checking pixels at various points
// Check center of the rectangle
await assertShapeExists(200, 175, 'Rectangle should be drawn at center');
// Check edges
await assertShapeExists(110, 110, 'Rectangle should exist at top-left');
await assertShapeExists(290, 110, 'Rectangle should exist at top-right');
await assertShapeExists(110, 240, 'Rectangle should exist at bottom-left');
await assertShapeExists(290, 240, 'Rectangle should exist at bottom-right');
});
it('should draw multiple rectangles without interference', async () => {
// Draw first rectangle
await drawRectangle(50, 50, 100, 100);
await assertShapeExists(100, 100, 'First rectangle should exist');
// Draw second rectangle in different location
await drawRectangle(300, 300, 100, 100);
await assertShapeExists(350, 350, 'Second rectangle should exist');
// Verify first rectangle still exists
await assertShapeExists(100, 100, 'First rectangle should still exist');
});
it('should draw small rectangles', async () => {
// Draw a small rectangle
await drawRectangle(400, 100, 20, 20);
await assertShapeExists(410, 110, 'Small rectangle should exist');
});
it('should draw large rectangles', async () => {
// Draw a large rectangle
await drawRectangle(50, 300, 400, 200);
await assertShapeExists(250, 400, 'Large rectangle should exist at center');
});
});
describe('Ellipse Tool', () => {
it('should draw an ellipse on the canvas', async () => {
// Draw an ellipse at (100, 100) with size 200x150
await drawEllipse(100, 100, 200, 150);
// Check center of the ellipse
await assertShapeExists(200, 175, 'Ellipse should be drawn at center');
});
it('should draw a circle (equal width and height)', async () => {
// Draw a circle
await drawEllipse(300, 100, 150, 150);
// Check center
await assertShapeExists(375, 175, 'Circle should exist at center');
});
it('should draw wide ellipses', async () => {
// Draw a wide ellipse
await drawEllipse(50, 400, 300, 100);
await assertShapeExists(200, 450, 'Wide ellipse should exist');
});
it('should draw tall ellipses', async () => {
// Draw a tall ellipse
await drawEllipse(500, 100, 100, 300);
await assertShapeExists(550, 250, 'Tall ellipse should exist');
});
});
describe('Mixed Shapes', () => {
it('should draw both rectangles and ellipses in the same scene', async () => {
// Draw a rectangle
await drawRectangle(100, 100, 150, 100);
// Draw an ellipse
await drawEllipse(300, 100, 150, 100);
// Verify both exist
await assertShapeExists(175, 150, 'Rectangle should exist');
await assertShapeExists(375, 150, 'Ellipse should exist');
});
});
});

View File

@ -0,0 +1,273 @@
/**
* Timeline animation tests for Lightningbeam
* Tests shape and object animations across keyframes
*/
import { describe, it, before } from 'mocha';
import { expect } from '@wdio/globals';
import { waitForAppReady } from '../helpers/app.js';
import {
drawRectangle,
drawEllipse,
clickCanvas,
dragCanvas,
selectMultipleShapes,
useKeyboardShortcut,
setPlayheadTime,
getPlayheadTime,
addKeyframe,
getPixelColor
} from '../helpers/canvas.js';
import { assertShapeExists } from '../helpers/assertions.js';
describe('Timeline Animation', () => {
before(async () => {
await waitForAppReady();
});
describe('Shape Keyframe Animation', () => {
it('should animate shape position across keyframes', async () => {
// Draw a rectangle at frame 1 (time 0)
await drawRectangle(100, 100, 100, 100);
// Select the shape by dragging a selection box over it
await selectMultipleShapes([{ x: 150, y: 150 }]);
await browser.pause(200);
// Verify it exists at original position
await assertShapeExists(150, 150, 'Shape should exist at frame 1');
// Move to frame 10 (time in seconds, assuming 30fps: frame 10 = 10/30 ≈ 0.333s)
await setPlayheadTime(0.333);
// Add a keyframe at this position
await addKeyframe();
await browser.pause(200);
// Shape is selected, so dragging from its center will move it
await dragCanvas(150, 150, 250, 150);
await browser.pause(300);
// At frame 10, shape should be at the new position (moved 100px to the right)
await assertShapeExists(250, 150, 'Shape should be at new position at frame 10');
// Go back to frame 1
await setPlayheadTime(0);
await browser.pause(200);
// Shape should be at original position
await assertShapeExists(150, 150, 'Shape should be at original position at frame 1');
// Go to middle frame (frame 5, time ≈ 0.166s)
await setPlayheadTime(0.166);
await browser.pause(200);
// Shape should be interpolated between the two positions
// At frame 5 (halfway), shape should be around x=200 (halfway between 150 and 250)
await assertShapeExists(200, 150, 'Shape should be interpolated at frame 5');
});
it('should modify shape edges when dragging edge of unselected shape', async () => {
// Draw a rectangle
await drawRectangle(400, 100, 100, 100);
// Get color at center
const centerColorBefore = await getPixelColor(450, 150);
// WITHOUT selecting the shape, drag the right edge
// The right edge is at x=500, so drag from there
await dragCanvas(500, 150, 550, 150);
await browser.pause(300);
// The shape should now be modified - it's been curved/stretched
// The original center should still have the shape
await assertShapeExists(450, 150, 'Center should still have shape');
// And there should be shape data extended to the right
const rightColorAfter = await getPixelColor(525, 150);
// This should have color (not white) since we dragged the edge there
expect(rightColorAfter.toLowerCase()).not.toBe('#ffffff');
});
it('should handle multiple keyframes on the same shape', async () => {
// Draw a shape
await drawRectangle(100, 300, 80, 80);
// Select it
await selectMultipleShapes([{ x: 140, y: 340 }]);
await browser.pause(200);
// Keyframe 1: time 0 (original position at x=140, y=340)
// Keyframe 2: time 0.333 (move right)
await setPlayheadTime(0.333);
await addKeyframe();
await browser.pause(200);
// Shape should still be selected, drag to move
await dragCanvas(140, 340, 200, 340);
await browser.pause(300);
// Keyframe 3: time 0.666 (move down but stay within canvas)
await setPlayheadTime(0.666);
await addKeyframe();
await browser.pause(200);
// Drag to move down (y=380 instead of 400 to stay in canvas)
await dragCanvas(200, 340, 200, 380);
await browser.pause(300);
// Verify positions at each keyframe
await setPlayheadTime(0);
await browser.pause(200);
await assertShapeExists(140, 340, 'Shape at keyframe 1 (x=140, y=340)');
await setPlayheadTime(0.333);
await browser.pause(200);
await assertShapeExists(200, 340, 'Shape at keyframe 2 (x=200, y=340)');
await setPlayheadTime(0.666);
await browser.pause(200);
await assertShapeExists(200, 380, 'Shape at keyframe 3 (x=200, y=380)');
// Check interpolation between keyframe 1 and 2 (at t=0.166, halfway)
await setPlayheadTime(0.166);
await browser.pause(200);
await assertShapeExists(170, 340, 'Shape interpolated between kf1 and kf2');
// Check interpolation between keyframe 2 and 3 (at t=0.5, halfway)
await setPlayheadTime(0.5);
await browser.pause(200);
await assertShapeExists(200, 360, 'Shape interpolated between kf2 and kf3');
});
});
describe('Group/Object Animation', () => {
it('should animate group position across keyframes', async () => {
// Create a group with two shapes
await drawRectangle(300, 300, 60, 60);
await drawRectangle(380, 300, 60, 60);
await selectMultipleShapes([
{ x: 330, y: 330 },
{ x: 410, y: 330 }
]);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Verify both shapes exist at frame 1
await assertShapeExists(330, 330, 'First shape at frame 1');
await assertShapeExists(410, 330, 'Second shape at frame 1');
// Select the group by dragging a selection box over it
await selectMultipleShapes([{ x: 370, y: 330 }]);
await browser.pause(200);
// Move to frame 10 and add keyframe
await setPlayheadTime(0.333);
await addKeyframe();
await browser.pause(200);
// Group is selected, so dragging will move it
// Drag from center of group down (but keep it within canvas bounds)
await dragCanvas(370, 330, 370, 380);
await browser.pause(300);
// At frame 10, group should be at new position (moved 50px down)
await assertShapeExists(330, 380, 'First shape at new position at frame 10');
await assertShapeExists(410, 380, 'Second shape at new position at frame 10');
// Go to frame 1
await setPlayheadTime(0);
await browser.pause(200);
// Group should be at original position
await assertShapeExists(330, 330, 'First shape at original position at frame 1');
await assertShapeExists(410, 330, 'Second shape at original position at frame 1');
// Go to frame 5 (middle, t=0.166)
await setPlayheadTime(0.166);
await browser.pause(200);
// Group should be interpolated (halfway between y=330 and y=380, so y=355)
await assertShapeExists(330, 355, 'First shape interpolated at frame 5');
await assertShapeExists(410, 355, 'Second shape interpolated at frame 5');
});
it('should maintain relative positions of shapes within animated group', async () => {
// Create a group (using safer y coordinates)
await drawRectangle(100, 250, 50, 50);
await drawRectangle(170, 250, 50, 50);
await selectMultipleShapes([
{ x: 125, y: 275 },
{ x: 195, y: 275 }
]);
await useKeyboardShortcut('g', true);
await browser.pause(300);
// Select group
await selectMultipleShapes([{ x: 160, y: 275 }]);
await browser.pause(200);
// Add keyframe and move
await setPlayheadTime(0.333);
await addKeyframe();
await browser.pause(200);
await dragCanvas(160, 275, 260, 275);
await browser.pause(300);
// At both keyframes, shapes should maintain 70px horizontal distance
await setPlayheadTime(0);
await browser.pause(200);
await assertShapeExists(125, 275, 'First shape at frame 1');
await assertShapeExists(195, 275, 'Second shape at frame 1 (70px apart)');
await setPlayheadTime(0.333);
await browser.pause(200);
// Both shapes moved 100px to the right
await assertShapeExists(225, 275, 'First shape at frame 10');
await assertShapeExists(295, 275, 'Second shape at frame 10 (still 70px apart)');
});
});
describe('Interpolation', () => {
it('should smoothly interpolate between keyframes', async () => {
// Draw a simple shape
await drawRectangle(500, 100, 50, 50);
// Select it
await selectMultipleShapes([{ x: 525, y: 125 }]);
await browser.pause(200);
// Keyframe at start (x=525)
await setPlayheadTime(0);
await browser.pause(100);
// Keyframe at end (1 second = frame 30, move to x=725)
await setPlayheadTime(1.0);
await addKeyframe();
await browser.pause(200);
await dragCanvas(525, 125, 725, 125);
await browser.pause(300);
// Check multiple intermediate frames for smooth interpolation
// Total movement: 200px over 1 second
// At 25% (0.25s), x should be 525 + 50 = 575
await setPlayheadTime(0.25);
await browser.pause(200);
await assertShapeExists(575, 125, 'Shape at 25% interpolation');
// At 50% (0.5s), x should be 525 + 100 = 625
await setPlayheadTime(0.5);
await browser.pause(200);
await assertShapeExists(625, 125, 'Shape at 50% interpolation');
// At 75% (0.75s), x should be 525 + 150 = 675
await setPlayheadTime(0.75);
await browser.pause(200);
await assertShapeExists(675, 125, 'Shape at 75% interpolation');
});
});
});