Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a13ff37
feat: add thumb layers to global testing types
gustedeveloper Jul 17, 2025
349e06c
feat: expose thumb layers in ThumbPage component for testing
gustedeveloper Jul 17, 2025
f04e1f6
feat: add thumb testing helpers with flat structure support and async…
gustedeveloper Jul 17, 2025
c579fe2
test: add dropped-button-visible-on-thumb test
gustedeveloper Jul 17, 2025
dc33c00
Merge pull request #767 from Lemoncode/feature/#544-drop-button-and-v…
brauliodiez Jul 17, 2025
a97c5d4
test: verify delete thumb page button is disabled when only one page …
gustedeveloper Jul 21, 2025
57ae713
test: verify shapes drop in correct thumb page and remain isolated ac…
gustedeveloper Jul 21, 2025
6600787
Merge pull request #769 from Lemoncode/feature/#551-change-thumb-sele…
brauliodiez Jul 21, 2025
408d72c
Merge pull request #768 from Lemoncode/feature/#549-delete-when-only-…
brauliodiez Jul 21, 2025
b777a93
test: verify that background color changes apply to all selected shapes
gustedeveloper Jul 23, 2025
3710ebb
add getShapeBackgroundColor helper for property testing
gustedeveloper Jul 23, 2025
a1239f3
feat: add helpers for adding components from different categories and…
gustedeveloper Jul 24, 2025
b08baba
refactor(test): use reusable helpers to add components and select all…
gustedeveloper Jul 24, 2025
c463cb7
feat: add utility to check if component properties are visible in the UI
gustedeveloper Jul 24, 2025
f0e6067
test: verify common properties are shown when multiple components are…
gustedeveloper Jul 24, 2025
565061b
refactor: extract target position calculation into utility function
gustedeveloper Jul 24, 2025
f4bd853
Merge pull request #770 from Lemoncode/feature/#606-multiple-selectio…
brauliodiez Jul 24, 2025
7d83fb9
feat: add utility to check if component properties are NOT visible in…
gustedeveloper Jul 24, 2025
0081392
test: verify that no props are shown when components have no props in…
gustedeveloper Jul 24, 2025
b40b0fd
feat: add test for thumbpage duplication via arrow icon
gustedeveloper Jul 25, 2025
9d90d56
fix: resolve intermittent failure in duplicate thumb page test by add…
gustedeveloper Jul 29, 2025
d3aba75
feat: add test for thumbpage duplication via right click
gustedeveloper Jul 29, 2025
28fb50a
test: add thumb page deletion test with thumb page count verification
gustedeveloper Jul 29, 2025
6c0ccc5
Merge pull request #774 from Lemoncode/feature/#548-delete-given-thum…
brauliodiez Jul 29, 2025
85addf7
Merge pull request #773 from Lemoncode/feature/#547-duplicate-page-ri…
brauliodiez Jul 29, 2025
64694b4
Merge pull request #772 from Lemoncode/feature/#546-thumbpage-duplica…
brauliodiez Jul 29, 2025
cadb555
Merge pull request #771 from Lemoncode/feature/#607-multiple-selectio…
brauliodiez Jul 29, 2025
2700928
feat: create fab button svg and add it to rich components gallery data
gustedeveloper Jul 31, 2025
9d1c77e
feat: implement fab button component
gustedeveloper Jul 31, 2025
3273819
feat: implement renderer for fab button shape
gustedeveloper Jul 31, 2025
f75e20f
feat: integrate fab button into shape system
gustedeveloper Jul 31, 2025
7bca48f
refactor: extract loadSvgWithFill to shared svg.utils and update icon…
gustedeveloper Aug 6, 2025
a32f1cc
Merge pull request #775 from Lemoncode/feature/#689-create-fab-button
brauliodiez Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
46 changes: 46 additions & 0 deletions e2e/helpers/konva-testing.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Group } from 'konva/lib/Group';
import { E2E_CanvasItemKeyAttrs } from './types/e2e-types';
import { getCanvasBoundingBox } from './position.helpers';

// MAIN CANVAS HELPERS

const getLayer = async (page: Page): Promise<Layer> =>
await page.evaluate(() => {
return window.__TESTING_KONVA_LAYER__;
Expand Down Expand Up @@ -50,6 +52,50 @@ export const getByShapeType = async (
}
};

// THUMB HELPERS

const getThumbLayer = async (page: Page, pageIndex: number): Promise<Layer> =>
await page.evaluate(index => {
return window.__TESTING_KONVA_THUMB_LAYERS__?.[index];
}, pageIndex);

const getThumbChildren = async (page: Page, pageIndex: number) => {
const layer = await getThumbLayer(page, pageIndex);
return layer?.children || [];
};

// Waits for a thumb to finish rendering (until it has at least one child)

export const waitForThumbToRender = async (
page: Page,
pageIndex: number,
timeout = 5000
) => {
await page.waitForFunction(
async index => {
const layer = window.__TESTING_KONVA_THUMB_LAYERS__?.[index];
if (!layer) return false;

const children = layer.children || [];
return children && children.length > 0;
},
pageIndex,
{ timeout }
);
};

export const getByShapeTypeInThumb = async (
page: Page,
pageIndex: number,
shape: string
): Promise<Group | Shape | undefined> => {
await waitForThumbToRender(page, pageIndex);

// Search for the shape
const children = await getThumbChildren(page, pageIndex);
return children?.find(child => child.attrs.shapeType === shape);
};

export const getTransformer = async (page: Page): Promise<any> => {
const layer = await getLayer(page);
const transformer = layer?.children.find((child: any) => {
Expand Down
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
31 changes: 31 additions & 0 deletions e2e/helpers/properties.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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();
}
};

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

test('when selecting button and bar chart, check that there are not common props (just default layering prop)', async ({
page,
}) => {
page.goto('');

// Add components to canvas
const components: ComponentWithCategory[] = [
{ name: 'Button' },
{ name: 'Bar Chart', category: 'Rich Components' },
];
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 buttonProps: string[] = [
'Stroke',
'Stroke style',
'Background',
'TextColor',
'Disabled',
'Border-radius',
];

// Verify button props are not visible in the properties panel
await checkPropertiesDoNotExist(page, buttonProps);

// Verify layering prop to be visible

await checkPropertiesExist(page, ['Layering']);
});
Loading