test: Add performance tests to Playwright (#17574)

This commit is contained in:
shortstacked
2025-07-25 15:45:40 +01:00
committed by GitHub
parent 70eab1b2a0
commit d29e583c45
7 changed files with 435 additions and 4 deletions

View File

@@ -0,0 +1,118 @@
# Performance Testing Helper
A simple toolkit for measuring and asserting performance in Playwright tests.
## Quick Start
### "I just want to measure how long something takes"
```typescript
const duration = await measurePerformance(page, 'open-node', async () => {
await n8n.canvas.openNode('Code');
});
console.log(`Opening node took ${duration.toFixed(1)}ms`);
```
### "I want to ensure an action completes within a time limit"
```typescript
const openNodeDuration = await measurePerformance(page, 'open-node', async () => {
await n8n.canvas.openNode('Code');
});
expect(openNodeDuration).toBeLessThan(2000); // Must complete in under 2 seconds
```
### "I want to measure the same action multiple times"
```typescript
const stats = [];
for (let i = 0; i < 20; i++) {
const duration = await measurePerformance(page, `open-node-${i}`, async () => {
await n8n.canvas.openNode('Code');
});
await n8n.ndv.clickBackToCanvasButton();
stats.push(duration);
}
const average = stats.reduce((a, b) => a + b, 0) / stats.length;
console.log(`Average: ${average.toFixed(1)}ms`);
expect(average).toBeLessThan(2000);
```
### "I want to set performance budgets for different actions"
```typescript
const budgets = {
triggerWorkflow: 8000, // 8 seconds
openLargeNode: 2500, // 2.5 seconds
};
// Measure workflow execution
const triggerDuration = await measurePerformance(page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
});
expect(triggerDuration).toBeLessThan(budgets.triggerWorkflow);
// Measure node opening
const openDuration = await measurePerformance(page, 'open-large-node', async () => {
await n8n.canvas.openNode('Code');
});
expect(openDuration).toBeLessThan(budgets.openLargeNode);
```
### "I want to test performance with different data sizes"
```typescript
const testData = [
{ size: 30000, budgets: { triggerWorkflow: 8000, openLargeNode: 2500 } },
{ size: 60000, budgets: { triggerWorkflow: 15000, openLargeNode: 6000 } },
];
testData.forEach(({ size, budgets }) => {
test(`performance - ${size.toLocaleString()} items`, async ({ page }) => {
// Setup test with specific data size
await setupTest(size);
// Measure against size-specific budgets
const duration = await measurePerformance(page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful')
});
expect(duration).toBeLessThan(budgets.triggerWorkflow);
});
});
```
### "I want to see all performance metrics from my test"
```typescript
// After running various performance measurements...
const allMetrics = await getAllPerformanceMetrics(page);
console.log('All performance metrics:', allMetrics);
// Output: { 'open-node': 1234.5, 'save-workflow': 567.8, ... }
```
### "I want to attach performance results to my test report"
```typescript
const allMetrics = await getAllPerformanceMetrics(page);
await test.info().attach('performance-metrics', {
body: JSON.stringify({
dataSize: 30000,
metrics: allMetrics,
budgets: { triggerWorkflow: 8000, openLargeNode: 2500 },
passed: {
triggerWorkflow: allMetrics['trigger-workflow'] < 8000,
openNode: allMetrics['open-large-node'] < 2500,
}
}, null, 2),
contentType: 'application/json',
});
```
## API Reference
### `measurePerformance(page, actionName, actionFn)`
Measures the duration of an async action using the Performance API.
- **Returns:** `Promise<number>` - Duration in milliseconds
### `getAllPerformanceMetrics(page)`
Retrieves all performance measurements from the current page.
- **Returns:** `Promise<Record<string, number>>` - Map of action names to durations
## Tips
- Use unique names for measurements in loops (e.g., `open-node-${i}`) to avoid conflicts
- Set realistic budgets - add some buffer to account for variance
- Consider different budgets for different data sizes or environments

View File

@@ -0,0 +1,127 @@
import { test, expect } from '../../fixtures/base';
import type { n8nPage } from '../../pages/n8nPage';
import { getAllPerformanceMetrics, measurePerformance } from '../../utils/performance-helper';
async function setupPerformanceTest(n8n: n8nPage, size: number) {
await n8n.goHome();
await n8n.workflows.clickNewWorkflowCard();
await n8n.workflows.importWorkflow('large.json', 'Large Workflow');
await n8n.notifications.closeNotificationByText('Successful');
// Configure data size
await n8n.canvas.openNode('Edit Fields');
await n8n.page
.getByTestId('parameter-input-value')
.getByTestId('parameter-input-field')
.fill(size.toString());
await n8n.ndv.clickBackToCanvasButton();
}
test.describe('Performance Example: Multiple sets}', () => {
const testData = [
{
size: 30000,
timeout: 40000,
budgets: {
triggerWorkflow: 8000, // 8s budget (actual: 6.4s)
openLargeNode: 2500, // 2.5s budget (actual: 1.6s)
},
},
{
size: 60000,
timeout: 60000,
budgets: {
triggerWorkflow: 15000, // 15s budget (actual: 12.4s)
openLargeNode: 6000, // 6s budget (actual: 4.9s)
},
},
];
testData.forEach(({ size, timeout, budgets }) => {
test(`workflow performance - ${size.toLocaleString()} items @db:reset`, async ({ n8n }) => {
test.setTimeout(timeout);
// Setup workflow
await setupPerformanceTest(n8n, size);
// Measure workflow execution
const triggerDuration = await measurePerformance(n8n.page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful', {
timeout: budgets.triggerWorkflow + 5000, // Add buffer
});
});
// Assert trigger performance
expect(triggerDuration).toBeLessThan(budgets.triggerWorkflow);
console.log(
`✓ Trigger workflow (${size} items): ${triggerDuration.toFixed(1)}ms < ${budgets.triggerWorkflow}ms`,
);
// Measure node opening
const openNodeDuration = await measurePerformance(n8n.page, 'open-large-node', async () => {
await n8n.canvas.openNode('Code');
});
// Assert node opening performance
expect(openNodeDuration).toBeLessThan(budgets.openLargeNode);
console.log(
`✓ Open node (${size} items): ${openNodeDuration.toFixed(1)}ms < ${budgets.openLargeNode}ms`,
);
// Get all metrics and attach to test report
const allMetrics = await getAllPerformanceMetrics(n8n.page);
console.log(`\nAll performance metrics for ${size.toLocaleString()} items:`, allMetrics);
// Attach metrics to test report
await test.info().attach('performance-metrics', {
body: JSON.stringify(
{
dataSize: size,
metrics: allMetrics,
budgets,
passed: {
triggerWorkflow: triggerDuration < budgets.triggerWorkflow,
openNode: openNodeDuration < budgets.openLargeNode,
},
},
null,
2,
),
contentType: 'application/json',
});
});
});
});
test('Performance Example: Multiple Loops in a single test @db:reset', async ({ n8n }) => {
await setupPerformanceTest(n8n, 30000);
const loopSize = 20;
const stats = [];
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
for (let i = 0; i < loopSize; i++) {
// Measure node opening
const openNodeDuration = await measurePerformance(n8n.page, `open-node-${i}`, async () => {
await n8n.canvas.openNode('Code');
});
stats.push(openNodeDuration);
await n8n.ndv.clickBackToCanvasButton();
console.log(`✓ Open node (${i + 1} of ${loopSize}): ${openNodeDuration.toFixed(1)}ms`);
}
// Get the average of the stats
const average = stats.reduce((a, b) => a + b, 0) / stats.length;
console.log(`Average open node duration: ${average.toFixed(1)}ms`);
expect(average).toBeLessThan(2000);
});
test('Performance Example: Aserting on a performance metric @db:reset', async ({ n8n }) => {
await setupPerformanceTest(n8n, 30000);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
const openNodeDuration = await measurePerformance(n8n.page, 'open-node', async () => {
await n8n.canvas.openNode('Code');
});
expect(openNodeDuration).toBeLessThan(2000);
});