UI tests
This commit is contained in:
parent
97b9ff71b7
commit
5a72743209
|
|
@ -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
|
||||||
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue