mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
Fixes [BS-3161](https://linear.app/affine-design/issue/BS-3161/发现已连接的connector会响应对齐线) Fixes [BS-3337](https://linear.app/affine-design/issue/BS-3337/connector你肿么了) Fixes [BS-3334](https://linear.app/affine-design/issue/BS-3334/connector-不应该能够被拖拽) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Corrected typos related to label editing state, ensuring more reliable label editing and display for connectors. - Fixed logic in the auto-complete overlay, improving when overlays appear during hover actions. - **New Features** - Improved connector label handling by ensuring label state is preserved and restored during editing. - Enhanced connector movement behavior, allowing connectors to be moved only when appropriate elements are selected. - **Tests** - Added end-to-end tests to verify connector movement and selection behaviors for improved reliability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
440 lines
12 KiB
TypeScript
440 lines
12 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
|
|
import {
|
|
addBasicConnectorElement,
|
|
changeConnectorStrokeColor,
|
|
changeConnectorStrokeStyle,
|
|
changeConnectorStrokeWidth,
|
|
createConnectorElement,
|
|
createShapeElement,
|
|
dragBetweenViewCoords,
|
|
edgelessCommonSetup as commonSetup,
|
|
getConnectorPath,
|
|
getConnectorPathWithInOut,
|
|
pickColorAtPoints,
|
|
rotateElementByHandle,
|
|
selectElementInEdgeless,
|
|
setEdgelessTool,
|
|
Shape,
|
|
toModelCoord,
|
|
toViewCoord,
|
|
triggerComponentToolbarAction,
|
|
triggerShapeSwitch,
|
|
} from '../../utils/actions/edgeless.js';
|
|
import { pressBackspace, waitNextFrame } from '../../utils/actions/index.js';
|
|
import {
|
|
assertConnectorPath,
|
|
assertEdgelessNonSelectedRect,
|
|
assertEdgelessSelectedRect,
|
|
getSelectedRect,
|
|
} from '../../utils/asserts.js';
|
|
import { test } from '../../utils/playwright.js';
|
|
|
|
test('path #1, the upper line is parallel with the lower line of antoher, and anchor from top to bottom of another', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createShapeElement(page, [200, -100], [300, 0], Shape.Square);
|
|
await createConnectorElement(page, [50, 0], [250, 0]);
|
|
|
|
await waitNextFrame(page);
|
|
await assertConnectorPath(page, [
|
|
[50, 0],
|
|
[50, -20],
|
|
[150, -20],
|
|
[150, 20],
|
|
[250, 20],
|
|
[250, 0],
|
|
]);
|
|
});
|
|
|
|
test('path #2, the top-right point is overlapped with the bottom-left point of another, and anchor from top to bottom of another', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createShapeElement(page, [100, -100], [200, 0], Shape.Square);
|
|
await createConnectorElement(page, [50, 0], [150, 0]);
|
|
|
|
await assertConnectorPath(page, [
|
|
[50, 0],
|
|
[50, -120],
|
|
[220, -120],
|
|
[220, 20],
|
|
[150, 20],
|
|
[150, 0],
|
|
]);
|
|
});
|
|
|
|
test('path #3, the two shape are parallel in x axis, the anchor from the right to right', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
|
|
await createConnectorElement(page, [100, 50], [300, 50]);
|
|
await assertConnectorPath(page, [
|
|
[100, 50],
|
|
[150, 50],
|
|
[150, 120],
|
|
[320, 120],
|
|
[320, 50],
|
|
[300, 50],
|
|
]);
|
|
});
|
|
|
|
test('when element is removed, connector should be deleted too', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createConnectorElement(page, [100, 50], [200, 0]);
|
|
|
|
//select
|
|
await dragBetweenViewCoords(page, [10, -10], [20, 20]);
|
|
await pressBackspace(page);
|
|
await dragBetweenViewCoords(page, [100, 50], [0, 50]);
|
|
await assertEdgelessNonSelectedRect(page);
|
|
});
|
|
|
|
test('connector connects triangle shape', async ({ page }) => {
|
|
await commonSetup(page);
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Triangle);
|
|
await createConnectorElement(page, [75, 50], [100, 50]);
|
|
|
|
await assertConnectorPath(page, [
|
|
[75, 50],
|
|
[100, 50],
|
|
]);
|
|
});
|
|
|
|
test('connector connects diamond shape', async ({ page }) => {
|
|
await commonSetup(page);
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
|
|
await createConnectorElement(page, [100, 50], [200, 50]);
|
|
|
|
await assertConnectorPath(page, [
|
|
[100, 50],
|
|
[200, 50],
|
|
]);
|
|
});
|
|
|
|
test('connector connects rotated Square shape', async ({ page }) => {
|
|
await commonSetup(page);
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createConnectorElement(page, [50, 0], [50, -100]);
|
|
await dragBetweenViewCoords(page, [-10, 50], [60, 60]);
|
|
await rotateElementByHandle(page, 30, 'top-left');
|
|
await assertConnectorPath(page, [
|
|
[75, 6.7],
|
|
[75, -46.65],
|
|
[50, -46.65],
|
|
[50, -100],
|
|
]);
|
|
await rotateElementByHandle(page, 30, 'top-left');
|
|
await assertConnectorPath(page, [
|
|
[93.3, 25],
|
|
[138.3, 25],
|
|
[138.3, -38.3],
|
|
[50, -38.3],
|
|
[50, -100],
|
|
]);
|
|
});
|
|
|
|
test('change connector line width', async ({ page }) => {
|
|
await commonSetup(page);
|
|
|
|
const start = { x: 100, y: 200 };
|
|
const end = { x: 300, y: 300 };
|
|
await addBasicConnectorElement(page, start, end);
|
|
|
|
await page.mouse.click(start.x + 5, start.y);
|
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
await changeConnectorStrokeColor(page, 'MediumGrey');
|
|
|
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
await changeConnectorStrokeWidth(page, 5);
|
|
|
|
const pickedColor = await pickColorAtPoints(page, [
|
|
[start.x + 5, start.y],
|
|
[start.x + 10, start.y],
|
|
]);
|
|
expect(pickedColor[0]).toBe(pickedColor[1]);
|
|
});
|
|
|
|
test('change connector stroke style', async ({ page }) => {
|
|
await commonSetup(page);
|
|
|
|
const start = { x: 100, y: 200 };
|
|
const end = { x: 300, y: 300 };
|
|
await addBasicConnectorElement(page, start, end);
|
|
|
|
await page.mouse.click(start.x + 5, start.y);
|
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
await changeConnectorStrokeColor(page, 'MediumGrey');
|
|
|
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
await changeConnectorStrokeStyle(page, 'dash');
|
|
await waitNextFrame(page);
|
|
|
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
|
|
const pickedColor = await pickColorAtPoints(page, [[start.x + 20, start.y]]);
|
|
expect(pickedColor[0]).toBe('#000000');
|
|
});
|
|
|
|
test.describe('quick connect', () => {
|
|
test('should create a connector when clicking on button', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
const [x, y] = await toViewCoord(page, [50, 50]);
|
|
await page.mouse.click(x, y);
|
|
|
|
const quickConnectBtn = page.getByRole('button', {
|
|
name: 'Draw connector',
|
|
});
|
|
|
|
await expect(quickConnectBtn).toBeVisible();
|
|
await quickConnectBtn.click();
|
|
await expect(quickConnectBtn).toBeHidden();
|
|
|
|
await assertConnectorPath(page, [
|
|
[100, 50],
|
|
[x, y],
|
|
]);
|
|
});
|
|
|
|
test('should be uncreated if the target is not found after clicking', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
const [x, y] = await toViewCoord(page, [50, 50]);
|
|
await page.mouse.click(x, y);
|
|
|
|
const quickConnectBtn = page.getByRole('button', {
|
|
name: 'Draw connector',
|
|
});
|
|
|
|
const bounds = await quickConnectBtn.boundingBox();
|
|
if (!bounds) {
|
|
throw new Error('bounds is not found');
|
|
}
|
|
|
|
await quickConnectBtn.click();
|
|
|
|
await page.mouse.click(bounds.x, bounds.y);
|
|
await assertEdgelessSelectedRect(page, [x - 50, y - 50, 100, 100]);
|
|
});
|
|
|
|
test('should be uncreated if the target is not found after pressing ESC', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
|
|
// select shape
|
|
const [x, y] = await toViewCoord(page, [50, 50]);
|
|
await page.mouse.click(x, y);
|
|
|
|
// click button
|
|
await triggerComponentToolbarAction(page, 'quickConnect');
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
await assertEdgelessNonSelectedRect(page);
|
|
});
|
|
|
|
test('should be connected if the target is found', async ({ page }) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
|
|
|
|
// select shape
|
|
const [x, y] = await toViewCoord(page, [50, 50]);
|
|
await page.mouse.click(x, y);
|
|
|
|
// click button
|
|
await triggerComponentToolbarAction(page, 'quickConnect');
|
|
|
|
// click target
|
|
const [tx, ty] = await toViewCoord(page, [200, 50]);
|
|
await page.mouse.click(tx, ty);
|
|
|
|
await assertConnectorPath(page, [
|
|
[100, 50],
|
|
[200, 50],
|
|
]);
|
|
});
|
|
|
|
test('should follow the mouse to automatically select the starting point', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
|
const shapeBounds = await toViewCoord(page, [0, 0]);
|
|
|
|
// select shape
|
|
const [x, y] = await toViewCoord(page, [50, 50]);
|
|
await page.mouse.click(x, y);
|
|
|
|
// click button
|
|
const quickConnectBtn = page.getByRole('button', {
|
|
name: 'Draw connector',
|
|
});
|
|
const bounds = await quickConnectBtn.boundingBox();
|
|
if (!bounds) {
|
|
throw new Error('bounds is not found');
|
|
}
|
|
await quickConnectBtn.click();
|
|
|
|
// at right
|
|
let point: [number, number] = [bounds.x, bounds.y];
|
|
let endpoint = await toModelCoord(page, point);
|
|
await assertConnectorPath(page, [[100, 50], endpoint]);
|
|
|
|
// at top
|
|
point = [shapeBounds[0] + 50, shapeBounds[1] - 50];
|
|
endpoint = await toModelCoord(page, point);
|
|
await page.mouse.move(...point);
|
|
await waitNextFrame(page);
|
|
await assertConnectorPath(page, [[50, 0], endpoint]);
|
|
|
|
// at left
|
|
point = [shapeBounds[0] - 50, shapeBounds[1] + 50];
|
|
endpoint = await toModelCoord(page, point);
|
|
await page.mouse.move(...point);
|
|
await assertConnectorPath(page, [[0, 50], endpoint]);
|
|
|
|
// at bottom
|
|
point = [shapeBounds[0] + 50, shapeBounds[1] + 100 + 50];
|
|
endpoint = await toModelCoord(page, point);
|
|
await page.mouse.move(...point);
|
|
await assertConnectorPath(page, [[50, 100], endpoint]);
|
|
});
|
|
|
|
test('the triangle connectors should remain the same when switch to other shape', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
const shape1Id = await createShapeElement(
|
|
page,
|
|
[0, 0],
|
|
[100, 100],
|
|
Shape.Triangle
|
|
);
|
|
const shape2Id = await createShapeElement(
|
|
page,
|
|
[200, 0],
|
|
[300, 100],
|
|
Shape.Triangle
|
|
);
|
|
|
|
await setEdgelessTool(page, 'connector');
|
|
await dragBetweenViewCoords(page, [60, 50], [240, 50]);
|
|
|
|
{
|
|
const path = await getConnectorPath(page);
|
|
// make sure the connector is created
|
|
expect(path.length).toBeGreaterThan(0);
|
|
}
|
|
|
|
// switch to other shape
|
|
|
|
await selectElementInEdgeless(page, [shape1Id]);
|
|
await triggerShapeSwitch(page, 'Square');
|
|
|
|
await selectElementInEdgeless(page, [shape2Id]);
|
|
await triggerShapeSwitch(page, 'Square');
|
|
|
|
await dragBetweenViewCoords(page, [50, 50], [0, 0]);
|
|
|
|
await dragBetweenViewCoords(page, [250, 50], [300, 50]);
|
|
|
|
{
|
|
const path = await getConnectorPathWithInOut(page);
|
|
expect(path.length).toBeGreaterThan(0);
|
|
path.forEach(point => {
|
|
[0, 1].forEach(i => {
|
|
expect(point.in[i]).not.toBeNaN();
|
|
expect(point.out[i]).not.toBeNaN();
|
|
expect(point.point[i]).not.toBeNaN();
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
test('connector can not be moved directly if the source or target is not selected', async ({
|
|
page,
|
|
}) => {
|
|
await commonSetup(page);
|
|
|
|
const normalConnectorId = await createConnectorElement(
|
|
page,
|
|
[0, 0],
|
|
[100, 100]
|
|
);
|
|
await selectElementInEdgeless(page, [normalConnectorId]);
|
|
|
|
const normalRect1 = await getSelectedRect(page);
|
|
|
|
// connector with no source and target can be moved
|
|
await dragBetweenViewCoords(page, [50, 50], [100, 100]);
|
|
const normalRect2 = await getSelectedRect(page);
|
|
expect(normalRect2).toEqual({
|
|
x: normalRect1.x + 50,
|
|
y: normalRect1.y + 50,
|
|
width: normalRect1.width,
|
|
height: normalRect1.height,
|
|
});
|
|
|
|
const shape1 = await createShapeElement(
|
|
page,
|
|
[150, 150],
|
|
[200, 200],
|
|
Shape.Square
|
|
);
|
|
const shape2 = await createShapeElement(
|
|
page,
|
|
[250, 250],
|
|
[300, 300],
|
|
Shape.Square
|
|
);
|
|
const connectorWithShapes = await createConnectorElement(
|
|
page,
|
|
[190, 175],
|
|
[260, 275]
|
|
);
|
|
await selectElementInEdgeless(page, [connectorWithShapes]);
|
|
|
|
// cannot be moved because the source and target are not selected
|
|
const initialShapeConnectorRect = await getSelectedRect(page);
|
|
await dragBetweenViewCoords(page, [225, 200], [275, 250]);
|
|
const shapeConnectorRect1 = await getSelectedRect(page);
|
|
expect(shapeConnectorRect1).toEqual(initialShapeConnectorRect);
|
|
|
|
// can be moved because the source and target are selected
|
|
await selectElementInEdgeless(page, [shape1, shape2, connectorWithShapes]);
|
|
await dragBetweenViewCoords(page, [225, 200], [275, 250]);
|
|
await selectElementInEdgeless(page, [connectorWithShapes]);
|
|
const shapeConnectorRect2 = await getSelectedRect(page);
|
|
expect(shapeConnectorRect2).toEqual({
|
|
x: initialShapeConnectorRect.x + 50,
|
|
y: initialShapeConnectorRect.y + 50,
|
|
width: initialShapeConnectorRect.width,
|
|
height: initialShapeConnectorRect.height,
|
|
});
|
|
});
|
|
});
|