Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
305 lines
7.9 KiB
TypeScript
305 lines
7.9 KiB
TypeScript
import { Page } from '@playwright/test'
|
|
|
|
export interface HeavyBoardOptions {
|
|
shapeCount?: number
|
|
includeGroups?: boolean
|
|
includeText?: boolean
|
|
includeArrows?: boolean
|
|
canvasWidth?: number
|
|
canvasHeight?: number
|
|
seed?: number // For reproducible layouts
|
|
}
|
|
|
|
export class HeavyBoardGenerator {
|
|
constructor(private page: Page) {}
|
|
|
|
async generateHeavyBoard(options: HeavyBoardOptions = {}): Promise<void> {
|
|
const {
|
|
shapeCount = 300,
|
|
includeGroups = true,
|
|
includeText = true,
|
|
includeArrows = true,
|
|
canvasWidth = 2000,
|
|
canvasHeight = 1500,
|
|
seed = 12345,
|
|
} = options
|
|
|
|
// Clear existing shapes first
|
|
await this.page.keyboard.press('Control+a')
|
|
await this.page.keyboard.press('Backspace')
|
|
|
|
// Wait for canvas to be ready
|
|
await this.page.waitForTimeout(100)
|
|
|
|
// Generate shapes using editor API injection
|
|
await this.page.evaluate(
|
|
({
|
|
shapeCount,
|
|
includeGroups: _includeGroups,
|
|
includeText,
|
|
includeArrows,
|
|
canvasWidth,
|
|
canvasHeight,
|
|
seed,
|
|
}) => {
|
|
const editor = (window as any).editor
|
|
if (!editor) {
|
|
throw new Error('Editor not found on window')
|
|
}
|
|
|
|
// Simple seeded random number generator for reproducible tests
|
|
class SeededRandom {
|
|
private seed: number
|
|
constructor(seed: number) {
|
|
this.seed = seed
|
|
}
|
|
|
|
next(): number {
|
|
this.seed = (this.seed * 9301 + 49297) % 233280
|
|
return this.seed / 233280
|
|
}
|
|
|
|
range(min: number, max: number): number {
|
|
return Math.floor(this.next() * (max - min + 1)) + min
|
|
}
|
|
|
|
choice<T>(array: T[]): T {
|
|
return array[this.range(0, array.length - 1)]
|
|
}
|
|
}
|
|
|
|
const random = new SeededRandom(seed)
|
|
const shapes: any[] = []
|
|
|
|
// Shape types and their weights
|
|
const shapeTypes = [
|
|
{ type: 'geo', weight: 50 }, // rectangles, ellipses, etc.
|
|
{ type: 'text', weight: includeText ? 25 : 0 },
|
|
{ type: 'arrow', weight: includeArrows ? 25 : 0 },
|
|
{ type: 'note', weight: 25 },
|
|
{ type: 'draw', weight: 25 },
|
|
].filter((s) => s.weight > 0)
|
|
|
|
// Generate base shapes
|
|
for (let i = 0; i < shapeCount; i++) {
|
|
const x = random.range(0, canvasWidth)
|
|
const y = random.range(0, canvasHeight)
|
|
const width = random.range(50, 200)
|
|
const height = random.range(30, 150)
|
|
|
|
// Pick shape type based on weights
|
|
let weightSum = 0
|
|
const targetWeight = random.next() * shapeTypes.reduce((sum, s) => sum + s.weight, 0)
|
|
const shapeType =
|
|
shapeTypes.find((s) => (weightSum += s.weight) >= targetWeight)?.type || 'geo'
|
|
|
|
let shape: any
|
|
|
|
switch (shapeType) {
|
|
case 'geo': {
|
|
const geoTypes = ['rectangle', 'ellipse', 'triangle', 'diamond', 'hexagon', 'star']
|
|
shape = {
|
|
id: `shape:${i}`,
|
|
type: 'geo',
|
|
x,
|
|
y,
|
|
props: {
|
|
w: width,
|
|
h: height,
|
|
geo: random.choice(geoTypes),
|
|
color: random.choice([
|
|
'black',
|
|
'blue',
|
|
'green',
|
|
'red',
|
|
'yellow',
|
|
'orange',
|
|
'violet',
|
|
]),
|
|
fill: random.choice(['none', 'semi', 'solid']),
|
|
size: random.choice(['s', 'm', 'l', 'xl']),
|
|
},
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'draw': {
|
|
// Create a simple drawn shape with some points
|
|
const numPoints = random.range(3, 8)
|
|
const points: Array<{ x: number; y: number; z: number }> = []
|
|
for (let p = 0; p < numPoints; p++) {
|
|
points.push({
|
|
x: random.range(0, width),
|
|
y: random.range(0, height),
|
|
z: 0.5,
|
|
})
|
|
}
|
|
|
|
// Access b64Vecs from the window.tldrawApi object
|
|
const tldrawApi = (window as any).tldrawApi
|
|
const b64VecsEncodePoints = tldrawApi.b64VecsEncodePoints
|
|
|
|
shape = {
|
|
id: `shape:${i}`,
|
|
type: 'draw',
|
|
x,
|
|
y,
|
|
props: {
|
|
segments: [
|
|
{
|
|
type: 'free',
|
|
path: b64VecsEncodePoints(points),
|
|
},
|
|
],
|
|
color: random.choice(['black', 'blue', 'green', 'red']),
|
|
size: random.choice(['s', 'm', 'l']),
|
|
},
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'text': {
|
|
const tldrawApi = (window as any).tldrawApi
|
|
const textSamples = [
|
|
'Performance Test',
|
|
'Lorem ipsum dolor sit amet',
|
|
'Heavy Board Test',
|
|
'Sample Text ' + i,
|
|
'Multi-line\nText Content',
|
|
'Longer text content for testing purposes',
|
|
]
|
|
shape = {
|
|
id: `shape:${i}`,
|
|
type: 'text',
|
|
x,
|
|
y,
|
|
props: {
|
|
richText: tldrawApi.toRichText(random.choice(textSamples)),
|
|
color: random.choice(['black', 'blue', 'green', 'red']),
|
|
size: random.choice(['s', 'm', 'l', 'xl']),
|
|
font: 'draw',
|
|
},
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'arrow': {
|
|
const endX = x + random.range(-200, 200)
|
|
const endY = y + random.range(-200, 200)
|
|
shape = {
|
|
id: `shape:${i}`,
|
|
type: 'arrow',
|
|
x,
|
|
y,
|
|
props: {
|
|
start: { x: 0, y: 0 },
|
|
end: { x: endX - x, y: endY - y },
|
|
color: random.choice(['black', 'blue', 'green', 'red']),
|
|
size: random.choice(['s', 'm', 'l']),
|
|
arrowheadStart: random.choice(['none', 'arrow', 'triangle']),
|
|
arrowheadEnd: random.choice(['arrow', 'triangle', 'dot']),
|
|
},
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'note': {
|
|
const tldrawApi = (window as any).tldrawApi
|
|
shape = {
|
|
id: `shape:${i}`,
|
|
type: 'note',
|
|
x,
|
|
y,
|
|
props: {
|
|
richText: tldrawApi.toRichText(`Note ${i}\nPerformance testing note`),
|
|
color: random.choice(['black', 'blue', 'green', 'red', 'yellow']),
|
|
size: random.choice(['s', 'm', 'l']),
|
|
},
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if (shape) {
|
|
shapes.push(shape)
|
|
}
|
|
}
|
|
|
|
// Create all shapes at once
|
|
editor.createShapes(shapes)
|
|
|
|
// Skip grouping for now - will add back later when debugging shape creation issues
|
|
// TODO: Re-enable grouping once basic shape generation is working
|
|
|
|
// Zoom to fit all content
|
|
editor.zoomToFit()
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Generated heavy board with ${shapes.length} shapes`)
|
|
},
|
|
{ shapeCount, includeGroups, includeText, includeArrows, canvasWidth, canvasHeight, seed }
|
|
)
|
|
|
|
// Wait for rendering to settle
|
|
await this.page.waitForTimeout(1000)
|
|
}
|
|
|
|
async setupStressTestBoard(shapeCount: number): Promise<void> {
|
|
// Pre-configured stress test with maximum complexity
|
|
await this.generateHeavyBoard({
|
|
shapeCount,
|
|
includeGroups: true,
|
|
includeText: true,
|
|
includeArrows: true,
|
|
canvasWidth: 3000,
|
|
canvasHeight: 2500,
|
|
seed: 42, // Fixed seed for consistent tests
|
|
})
|
|
}
|
|
|
|
async getShapeCount(): Promise<number> {
|
|
return await this.page.evaluate(() => {
|
|
const editor = (window as any).editor
|
|
return editor ? editor.getCurrentPageShapes().length : 0
|
|
})
|
|
}
|
|
|
|
async selectRandomShapes(count: number, seed: number = 12345): Promise<void> {
|
|
await this.page.evaluate(
|
|
({ count, seed }) => {
|
|
const editor = (window as any).editor
|
|
if (!editor) return
|
|
|
|
// Simple seeded random number generator for reproducible tests
|
|
class SeededRandom {
|
|
private seed: number
|
|
constructor(seed: number) {
|
|
this.seed = seed
|
|
}
|
|
|
|
next(): number {
|
|
this.seed = (this.seed * 9301 + 49297) % 233280
|
|
return this.seed / 233280
|
|
}
|
|
|
|
range(min: number, max: number): number {
|
|
return Math.floor(this.next() * (max - min + 1)) + min
|
|
}
|
|
}
|
|
|
|
const random = new SeededRandom(seed)
|
|
const allShapes = editor.getCurrentPageShapes()
|
|
const selectedCount = Math.min(count, allShapes.length)
|
|
|
|
// Select random shapes using seeded random
|
|
const shuffled = allShapes.sort(() => 0.5 - random.next())
|
|
const selected = shuffled.slice(0, selectedCount)
|
|
const shapeIds = selected.map((shape: any) => shape.id)
|
|
|
|
editor.setSelectedShapes(shapeIds)
|
|
},
|
|
{ count, seed }
|
|
)
|
|
}
|
|
}
|