Skip to content
Draft
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
151 changes: 151 additions & 0 deletions e2e/tests/highlight-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { expect, test } from "@playwright/test";

import { GraphPageObject } from "../page-objects/GraphPageObject";

const BLOCKS = [
{
id: "block-a",
is: "Block" as const,
x: 100,
y: 100,
width: 180,
height: 90,
name: "Block A",
anchors: [],
selected: false,
},
{
id: "block-b",
is: "Block" as const,
x: 380,
y: 100,
width: 180,
height: 90,
name: "Block B",
anchors: [],
selected: false,
},
];

const CONNECTIONS = [
{
id: "connection-a-b",
sourceBlockId: "block-a",
targetBlockId: "block-b",
},
];

type THighlightModesByEntity = Record<string, number | undefined>;

async function getModes(graphPO: GraphPageObject): Promise<THighlightModesByEntity> {
return graphPO.page.evaluate(() => {
const { GraphComponent } = window.GraphModule;
const components = window.graph.getElementsOverRect(
{
x: -100000,
y: -100000,
width: 200000,
height: 200000,
},
[GraphComponent],
) as Array<{
getEntityType(): string;
getEntityId(): string | number;
getHighlightVisualMode(): number | undefined;
}>;

const result: THighlightModesByEntity = {};
for (const component of components) {
result[`${component.getEntityType()}:${String(component.getEntityId())}`] = component.getHighlightVisualMode();
}

return result;
});
}

test.describe("HighlightService API", () => {
let graphPO: GraphPageObject;

test.beforeEach(async ({ page }) => {
graphPO = new GraphPageObject(page);
await graphPO.initialize({
blocks: BLOCKS,
connections: CONNECTIONS,
});
await graphPO.waitForFrames(4);
});

test("highlight() highlights only targets", async () => {
await graphPO.page.evaluate(() => {
window.graph.highlight({
block: ["block-a"],
});
});
await graphPO.waitForFrames(2);

const modes = await getModes(graphPO);

expect(modes["block:block-a"]).toBe(20);
expect(modes["block:block-b"]).toBeUndefined();
expect(modes["connection:connection-a-b"]).toBeUndefined();
});

test("focus() lowlights non-target entities", async () => {
await graphPO.page.evaluate(() => {
window.graph.focus({
block: ["block-a"],
});
});
await graphPO.waitForFrames(2);

const modes = await getModes(graphPO);

expect(modes["block:block-a"]).toBe(20);
expect(modes["block:block-b"]).toBe(10);
expect(modes["connection:connection-a-b"]).toBe(10);
});

test("clearHighlight() resets all highlight modes", async () => {
await graphPO.page.evaluate(() => {
window.graph.focus({
block: ["block-a"],
});
window.graph.clearHighlight();
});
await graphPO.waitForFrames(2);

const modes = await getModes(graphPO);

expect(modes["block:block-a"]).toBeUndefined();
expect(modes["block:block-b"]).toBeUndefined();
expect(modes["connection:connection-a-b"]).toBeUndefined();
});

test("emits highlight-changed for highlight, focus and clear", async () => {
const events = await graphPO.page.evaluate(() => {
const collected: Array<{ mode?: string; previousMode?: string }> = [];

const handler = (event: CustomEvent<{ mode?: string; previous?: { mode?: string } }>) => {
collected.push({
mode: event.detail.mode,
previousMode: event.detail.previous?.mode,
});
};

window.graph.on("highlight-changed", handler);

window.graph.highlight({ block: ["block-a"] });
window.graph.focus({ block: ["block-b"] });
window.graph.clearHighlight();

window.graph.off("highlight-changed", handler);

return collected;
});

expect(events).toHaveLength(3);
expect(events[0]).toEqual({ mode: "highlight", previousMode: undefined });
expect(events[1]).toEqual({ mode: "focus", previousMode: "highlight" });
expect(events[2]).toEqual({ mode: undefined, previousMode: "focus" });
});
});
167 changes: 167 additions & 0 deletions e2e/tests/related-entities.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { expect, test } from "@playwright/test";

import { GraphPageObject } from "../page-objects/GraphPageObject";

const BLOCKS = [
{
id: "block-a",
is: "Block" as const,
x: 100,
y: 100,
width: 180,
height: 90,
name: "Block A",
anchors: [],
selected: false,
},
{
id: "block-b",
is: "Block" as const,
x: 380,
y: 100,
width: 180,
height: 90,
name: "Block B",
anchors: [],
selected: false,
},
{
id: "block-c",
is: "Block" as const,
x: 660,
y: 100,
width: 180,
height: 90,
name: "Block C",
anchors: [],
selected: false,
},
];

const CONNECTIONS = [
{
id: "connection-a-b",
sourceBlockId: "block-a",
targetBlockId: "block-b",
},
{
id: "connection-b-c",
sourceBlockId: "block-b",
targetBlockId: "block-c",
},
];

async function getRelatedBlocksByDepth(
graphPO: GraphPageObject,
sourceBlockId: string,
depth: number,
): Promise<Array<string | number>> {
return graphPO.page.evaluate(
({ sourceId, depthLevel }) => {
const { GraphComponent } = window.GraphModule;
const graphComponents = window.graph.getElementsOverRect(
{
x: -100000,
y: -100000,
width: 200000,
height: 200000,
},
[GraphComponent],
) as Array<{
getEntityType(): string;
getEntityId(): string | number;
}>;

const source = graphComponents.find((component) => {
return component.getEntityType() === "block" && component.getEntityId() === sourceId;
});

if (!source) {
throw new Error(`Source block component not found: ${sourceId}`);
}

const related = window.graph.getRelatedEntitiesByPorts(source, { depth: depthLevel }) as Record<
string,
Array<string | number>
>;

return (related.block ?? []).slice().sort();
},
{ sourceId: sourceBlockId, depthLevel: depth },
);
}

async function getRelatedByDepth(
graphPO: GraphPageObject,
sourceBlockId: string,
depth: number,
): Promise<Record<string, Array<string | number>>> {
return graphPO.page.evaluate(
({ sourceId, depthLevel }) => {
const { GraphComponent } = window.GraphModule;
const graphComponents = window.graph.getElementsOverRect(
{
x: -100000,
y: -100000,
width: 200000,
height: 200000,
},
[GraphComponent],
) as Array<{
getEntityType(): string;
getEntityId(): string | number;
}>;

const source = graphComponents.find((component) => {
return component.getEntityType() === "block" && component.getEntityId() === sourceId;
});

if (!source) {
throw new Error(`Source block component not found: ${sourceId}`);
}

return window.graph.getRelatedEntitiesByPorts(source, { depth: depthLevel }) as Record<
string,
Array<string | number>
>;
},
{ sourceId: sourceBlockId, depthLevel: depth },
);
}

test.describe("Related entities by ports", () => {
let graphPO: GraphPageObject;

test.beforeEach(async ({ page }) => {
graphPO = new GraphPageObject(page);
await graphPO.initialize({
blocks: BLOCKS,
connections: CONNECTIONS,
});

await graphPO.waitForFrames(5);
});

test("depth=1 returns only directly connected blocks", async () => {
const related = await getRelatedBlocksByDepth(graphPO, "block-a", 1);

expect(related).toEqual(["block-b"]);
});

test("depth=2 traverses through connection to next block", async () => {
const related = await getRelatedBlocksByDepth(graphPO, "block-a", 2);

expect(related).toEqual(["block-b", "block-c"]);
});

test("connections are included but do not increment depth", async () => {
const depth1 = await getRelatedByDepth(graphPO, "block-a", 1);
const depth2 = await getRelatedByDepth(graphPO, "block-a", 2);

expect(depth1.block?.slice().sort()).toEqual(["block-b"]);
expect(depth1.connection?.slice().sort()).toEqual(["connection-a-b"]);

expect(depth2.block?.slice().sort()).toEqual(["block-b", "block-c"]);
expect(depth2.connection?.slice().sort()).toEqual(["connection-a-b", "connection-b-c"]);
});
});
10 changes: 10 additions & 0 deletions src/components/canvas/GraphComponent/GraphComponent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class TestGraphComponent extends GraphComponent {
return "test-id";
}

public getEntityType(): string {
return "test";
}

public subscribeGraphEvent<EventName extends keyof GraphEventsDefinitions>(
eventName: EventName,
handler: GraphEventsDefinitions[EventName],
Expand Down Expand Up @@ -42,6 +46,12 @@ function createTestComponent(root?: HTMLDivElement): TestSetup {
const hitTestRemove = jest.fn();
const fakeGraph = {
on: graphOn,
rootStore: {
highlightService: {
registerComponent: jest.fn(),
unregisterComponent: jest.fn(),
},
},
hitTest: {
remove: hitTestRemove,
update: jest.fn(),
Expand Down
Loading
Loading