Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 77 additions & 0 deletions e2e/page-objects/GraphCameraComponentObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,83 @@ export class GraphCameraComponentObject {
await this.graphPO.waitForFrames(3);
}

/**
* Pan the camera via trackpad wheel events so that the given world point ends up
* under the mouse cursor.
*
* handleTrackpadMove does camera.move(-deltaX, -deltaY), so to shift the camera by
* (dx, dy) screen pixels we pass wheel(dx, dy) directly. A non-zero deltaX triggers
* trackpad detection in isTrackpadWheelEvent() (horizontal scroll → trackpad).
*
* Fires multiple small steps (≤8 px) to stay within Camera's edge-guard limits.
*
* @param worldX - Target world X coordinate to bring under cursor
* @param worldY - Target world Y coordinate to bring under cursor
* @param viewportX - Cursor viewport X (defaults to canvas center)
* @param viewportY - Cursor viewport Y (defaults to canvas center)
*/
async panWorldPointUnderCursor(
worldX: number,
worldY: number,
viewportX?: number,
viewportY?: number
): Promise<void> {
const canvasBounds = await this.getCanvasBounds();
const vx = viewportX ?? canvasBounds.x + canvasBounds.width / 2;
const vy = viewportY ?? canvasBounds.y + canvasBounds.height / 2;

const delta = await this.page.evaluate(
({ wx, wy, vx, vy }) => {
const canvas = window.graph.getGraphCanvas();
const rect = canvas.getBoundingClientRect();
const [currentWX, currentWY] = window.graph.cameraService.getRelativeXY(
vx - rect.left,
vy - rect.top
);
const { scale } = window.graph.cameraService.getCameraState();
return {
dx: (wx - currentWX) * scale,
dy: (wy - currentWY) * scale,
};
},
{ wx: worldX, wy: worldY, vx, vy }
);

await this.page.mouse.move(vx, vy);

const STEP = 8;
const steps = Math.ceil(Math.max(Math.abs(delta.dx), Math.abs(delta.dy)) / STEP);
const stepDx = steps > 0 ? delta.dx / steps : 0;
const stepDy = steps > 0 ? delta.dy / steps : 0;

for (let i = 0; i < steps; i++) {
const wheelDx = stepDx !== 0 ? stepDx : 0.1;
await this.page.mouse.wheel(wheelDx, stepDy);
await this.graphPO.waitForFrames(1);
}
}

/**
* Pan the camera via trackpad wheel events by the given screen-pixel amount.
* Positive dx moves content to the left (camera right), positive dy moves content up.
* Mouse must already be positioned on the canvas before calling.
*
* @param dx - Horizontal pan amount in screen pixels
* @param dy - Vertical pan amount in screen pixels
*/
async trackpadPan(dx: number, dy: number): Promise<void> {
const STEP = 8;
const totalSteps = Math.ceil(Math.max(Math.abs(dx), Math.abs(dy)) / STEP);
const stepDx = totalSteps > 0 ? dx / totalSteps : 0;
const stepDy = totalSteps > 0 ? dy / totalSteps : 0;

for (let moved = 0; moved < totalSteps; moved++) {
const wheelDx = stepDx !== 0 ? -stepDx : -0.1;
await this.page.mouse.wheel(wheelDx, -stepDy);
await this.graphPO.waitForFrames(1);
}
}

/**
* Emulate camera pan with mouse drag
* @param deltaX - Horizontal drag distance in pixels
Expand Down
93 changes: 93 additions & 0 deletions e2e/page-objects/GraphPageObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,65 @@ import { GraphBlockComponentObject } from "./GraphBlockComponentObject";
import { GraphConnectionComponentObject } from "./GraphConnectionComponentObject";
import { GraphCameraComponentObject } from "./GraphCameraComponentObject";

let listenerIdCounter = 0;

/**
* Collects graph events fired in the browser context and allows analyzing them
* via a callback that runs in the browser — no DOM serialization needed.
*
* Usage:
* const listener = await graphPO.listenGraphEvents("mouseenter");
* // ... trigger actions ...
* const ids = await listener.analyze((events) =>
* events.map((e) => e.detail?.target?.props?.id).filter(Boolean)
* );
*/
export class GraphEventListener<TDetail = unknown> {
private readonly storageKey: string;

constructor(
private readonly page: Page,
storageKey: string
) {
this.storageKey = storageKey;
}

/**
* Runs `fn` inside the browser with the collected events array as the first argument.
* Additional serializable values from Node.js can be passed as extra arguments.
* Returns whatever `fn` returns (must be serializable).
*/
async analyze<TResult, TArgs extends unknown[]>(
fn: (events: CustomEvent<TDetail>[], ...args: TArgs) => TResult,
...args: TArgs
): Promise<TResult> {
return this.page.evaluate(
({ key, fnStr, args }) => {
const events = (window as any)[key] ?? [];
// eslint-disable-next-line no-new-func
return new Function("events", "...args", `return (${fnStr})(events, ...args)`)(
events,
...args
);
},
{ key: this.storageKey, fnStr: fn.toString(), args }
);
}

/** Removes the event listener and cleans up the storage key from window. */
async stop(): Promise<void> {
await this.page.evaluate((key) => {
const handler = (window as any)[`${key}_handler`];
if (handler) {
window.graph.off((window as any)[`${key}_eventName`], handler);
}
delete (window as any)[key];
delete (window as any)[`${key}_handler`];
delete (window as any)[`${key}_eventName`];
}, this.storageKey);
}
}

export interface GraphConfig {
blocks?: TBlock[];
connections?: TConnection[];
Expand Down Expand Up @@ -390,4 +449,38 @@ export class GraphPageObject {
return window.getComputedStyle(root).cursor;
});
}

/**
* Starts collecting graph events of the given name in the browser context.
* Returns a {@link GraphEventListener} whose `analyze()` method lets you
* inspect the collected events inside the browser — no DOM serialization needed.
*
* @example
* const listener = await graphPO.listenGraphEvents("mouseenter");
* // ... trigger actions ...
* const ids = await listener.analyze((events) =>
* events.map((e) => e.detail?.target?.props?.id).filter(Boolean)
* );
* expect(ids).toContain("block-1");
*/
async listenGraphEvents<TDetail = unknown>(
eventName: string
): Promise<GraphEventListener<TDetail>> {
const key = `__graphListener_${listenerIdCounter++}_${eventName}`;

await this.page.evaluate(
({ key, eventName }) => {
(window as any)[key] = [];
(window as any)[`${key}_eventName`] = eventName;
const handler = (event: CustomEvent) => {
(window as any)[key].push(event);
};
(window as any)[`${key}_handler`] = handler;
window.graph.on(eventName as any, handler);
},
{ key, eventName }
);

return new GraphEventListener<TDetail>(this.page, key);
}
}
Loading
Loading