Skip to content
1 change: 1 addition & 0 deletions e2e/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './konva-testing.helpers';
export * from './position.helpers';
export * from './properties.helpers';
126 changes: 114 additions & 12 deletions e2e/helpers/position.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export interface Position {
y: number;
}

export interface ComponentWithCategory {
name: string;
category?: string;
}

export const getLocatorPosition = async (
locator: Locator
): Promise<Position> => {
Expand Down Expand Up @@ -44,6 +49,18 @@ export const dragAndDrop = async (
await page.mouse.up();
};

const getTargetPosition = (
canvasPosition: { x: number; y: number },
displacementQty: number,
multiplyFactor: number
): Position => {
const positionDisplacement = displacementQty * (multiplyFactor + 1);
return {
x: canvasPosition.x + displacementQty + positionDisplacement,
y: canvasPosition.y + positionDisplacement,
};
};

export const addComponentsToCanvas = async (
page: Page,
components: string[],
Expand All @@ -58,25 +75,110 @@ export const addComponentsToCanvas = async (
await component.scrollIntoViewIfNeeded();
const position = await getLocatorPosition(component);

const targetPosition = (
displacementQty: number,
multiplyFactor: number
) => {
const positionDisplacement = displacementQty * (multiplyFactor + 1);
return {
x: canvasPosition.x + displacementQty + positionDisplacement,
y: canvasPosition.y + positionDisplacement,
};
};

await dragAndDrop(page, position, targetPosition(displacementQty, index));
const targetPosition = getTargetPosition(
canvasPosition,
displacementQty,
index
);
await dragAndDrop(page, position, targetPosition);
}
};

export const addComponentsWithDifferentCategoriesToCanvas = async (
page: Page,
components: ComponentWithCategory[],
displacementQty: number = 120
) => {
// Handle empty array
if (components.length === 0) {
return;
}

const stageCanvas = await page.locator('#konva-stage canvas').nth(1);
const canvasPosition = await stageCanvas.boundingBox();
if (!canvasPosition) throw new Error('No canvas found');

let currentCategory: string | undefined = undefined;

for await (const [index, componentConfig] of components.entries()) {
try {
// Change category only if it's different from current one
if (
componentConfig.category &&
componentConfig.category !== currentCategory
) {
const categoryButton = page.getByText(componentConfig.category, {
exact: true,
});

// Check if category exists before clicking
await categoryButton.waitFor({ state: 'visible', timeout: 3000 });
await categoryButton.click();

// Wait a bit for the category change to take effect
await page.waitForTimeout(500);
currentCategory = componentConfig.category;
}

// Find component with better handling for duplicates
let component = page.getByAltText(componentConfig.name, { exact: true });

// Check if there are multiple elements with the same alt text
const componentCount = await component.count();

if (componentCount > 1) {
// Handle duplicates by selecting the first visible one in the current category context
console.warn(
`Multiple components found with name "${componentConfig.name}". Using first visible one.`
);
component = component.first();
}

// Wait for component to be available
await component.waitFor({ state: 'visible', timeout: 5000 });
await component.scrollIntoViewIfNeeded();
const position = await getLocatorPosition(component);

const targetPosition = getTargetPosition(
canvasPosition,
displacementQty,
index
);
await dragAndDrop(page, position, targetPosition);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to add component "${componentConfig.name}" from category "${componentConfig.category || 'default'}": ${errorMessage}`
);
}
}
};

export const getShapePosition = async (shape: Group): Promise<Position> => {
return { x: shape?.attrs.x, y: shape?.attrs.y };
};

export const selectAllComponentsInCanvas = async (
page: Page,
selectionArea?: { start: Position; end: Position }
) => {
// Clear any existing selection first
await page.mouse.click(800, 130);

// Small delay to ensure the click is processed
await page.waitForTimeout(100);

const selectionStart = selectionArea?.start || { x: 260, y: 130 };
const selectionEnd = selectionArea?.end || { x: 1000, y: 650 };

// Perform drag selection using the proven coordinates
await dragAndDrop(page, selectionStart, selectionEnd);

// Small delay to ensure selection is processed
await page.waitForTimeout(200);
};

export const moveSelected = (
page: Page,
direction: string,
Expand Down
21 changes: 21 additions & 0 deletions e2e/helpers/properties.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Page, expect } from '@playwright/test';
import { getByShapeType } from './konva-testing.helpers';
import { Group } from 'konva/lib/Group';

export const getShapeBackgroundColor = async (
page: Page,
shapeType: string
): Promise<string> => {
const shape = (await getByShapeType(page, shapeType)) as Group;
return shape?.children?.[0]?.attrs?.fill;
};

export const checkPropertiesExist = async (
page: Page,
properties: string[]
) => {
for (const property of properties) {
const propLocator = page.getByText(property, { exact: true });
await expect(propLocator).toBeVisible();
}
};
79 changes: 79 additions & 0 deletions e2e/props/multi-select-bg-and-common-props.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { test, expect } from '@playwright/test';
import {
getTransformer,
ComponentWithCategory,
getShapeBackgroundColor,
addComponentsWithDifferentCategoriesToCanvas,
selectAllComponentsInCanvas,
checkPropertiesExist,
} from '../helpers';

test('when selecting a button and a rectangle, select both, change background color to red, both should update their bg to red', async ({
page,
}) => {
await page.goto('');

// Add components to canvas
const components: ComponentWithCategory[] = [
{ name: 'Button' }, // Button is in default 'Components' category
{ name: 'Rectangle', category: 'Basic Shapes' },
];
await addComponentsWithDifferentCategoriesToCanvas(page, components);

// Select all components in canvas
await selectAllComponentsInCanvas(page);

// Confirm both items are selected
const selectedItems = await getTransformer(page);
expect(selectedItems._nodes.length).toEqual(2);

// Change background color to red
const bgSelector = page
.getByText('Background')
.locator('..')
.locator('button');
await bgSelector.click();

const redColorBox = page.locator(
'div[style*="background-color: rgb(221, 0, 0)"]'
);
await redColorBox.click();

// Verify that both items have red background
const buttonBgColor = await getShapeBackgroundColor(page, 'button');
const rectangleBgColor = await getShapeBackgroundColor(page, 'rectangle');

expect(buttonBgColor).toBe('#DD0000');
expect(rectangleBgColor).toBe('#DD0000');
});

test('verify that in the props we can find the common props of both items', async ({
page,
}) => {
await page.goto('');

// Add components to canvas
const components: ComponentWithCategory[] = [
{ name: 'Button' },
{ name: 'Rectangle', category: 'Basic Shapes' },
];
await addComponentsWithDifferentCategoriesToCanvas(page, components);

// Select all components in canvas
await selectAllComponentsInCanvas(page);

// Confirm both items are selected
const selectedItems = await getTransformer(page);
expect(selectedItems._nodes.length).toEqual(2);

const commonProps: string[] = [
'Layering',
'Stroke',
'Stroke style',
'Background',
'Border-radius',
];

// Verify common properties are visible in the properties panel
await checkPropertiesExist(page, commonProps);
});