Skip to content
Merged
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
25 changes: 24 additions & 1 deletion e2e/build-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const cssPlugin = {
},
};

esbuild
const baseBundle = esbuild
.build({
entryPoints: [path.join(__dirname, "entry.ts")],
bundle: true,
Expand All @@ -37,3 +37,26 @@ esbuild
console.error("E2E bundle failed:", err);
process.exit(1);
});

const reactBundle = esbuild
.build({
entryPoints: [path.join(__dirname, "react-entry.ts")],
bundle: true,
outfile: path.join(__dirname, "dist/graph-react.bundle.js"),
format: "iife",
globalName: "GraphModule",
platform: "browser",
target: ["es2020"],
sourcemap: true,
plugins: [cssPlugin],
jsx: "automatic",
})
.then(() => {
console.log("E2E React bundle created successfully with CSS");
})
.catch((err) => {
console.error("E2E React bundle failed:", err);
process.exit(1);
});

Promise.all([baseBundle, reactBundle]).catch(() => process.exit(1));
54 changes: 45 additions & 9 deletions e2e/page-objects/GraphPageObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,18 @@ export class GraphPageObject {
}

/**
* Initialize graph with config
* Returns the URL of the HTML page to navigate to for initialization.
* Override in subclasses to use a different page (e.g. /react.html).
*/
async initialize(config: GraphConfig): Promise<void> {
await this.page.goto("/base.html");

// Wait for Graph library to load from the HTML page
await this.page.waitForFunction(() => {
return (window as any).graphLibraryLoaded === true;
});
protected getUrl(): string {
return "/base.html";
}

// Initialize graph using the loaded module
/**
* Creates and configures the graph instance in the browser context.
* Override in subclasses to use a different rendering setup (e.g. React).
*/
protected async setupGraph(config: GraphConfig): Promise<void> {
await this.page.evaluate((cfg) => {
const rootEl = document.getElementById("root");
if (!rootEl) {
Expand All @@ -137,6 +138,20 @@ export class GraphPageObject {
window.graph = graph;
window.graphInitialized = true;
}, config);
}

/**
* Initialize graph with config
*/
async initialize(config: GraphConfig): Promise<void> {
await this.page.goto(this.getUrl());

// Wait for Graph library to load from the HTML page
await this.page.waitForFunction(() => {
return (window as any).graphLibraryLoaded === true;
});

await this.setupGraph(config);

// Wait for graph to be ready
await this.page.waitForFunction(
Expand All @@ -148,6 +163,15 @@ export class GraphPageObject {
await this.waitForFrames(3);
}

/**
* Set camera zoom to a specific scale level
*/
async setZoom(scale: number): Promise<void> {
await this.page.evaluate((s) => {
window.graph.zoom({ scale: s });
}, scale);
}

/**
* Wait for N animation frames to complete using graph's scheduler
* This is necessary because the library uses Scheduler with requestAnimationFrame
Expand Down Expand Up @@ -483,4 +507,16 @@ export class GraphPageObject {

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

/**
* Call setEntities on the graph with new blocks and connections
*/
async setEntities(config: GraphConfig): Promise<void> {
await this.page.evaluate((cfg) => {
window.graph.setEntities({
blocks: cfg.blocks || [],
connections: cfg.connections || [],
});
}, config);
}
}
98 changes: 98 additions & 0 deletions e2e/page-objects/ReactGraphPageObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Page } from "@playwright/test";
import { TBlock } from "../../src/components/canvas/blocks/Block";
import { TConnection } from "../../src/store/connection/ConnectionState";
import { GraphPageObject, GraphConfig } from "./GraphPageObject";

/**
* PageObject for React-based graph rendering (using GraphCanvas + BlocksList).
* Extends GraphPageObject with React-specific initialization.
*/
export class ReactGraphPageObject extends GraphPageObject {
constructor(page: Page) {
super(page);
}

protected getUrl(): string {
return "/react.html";
}

/**
* Creates graph wrapped in React GraphCanvas (enables HTML block rendering via BlocksList).
*/
protected async setupGraph(config: GraphConfig): Promise<void> {
await this.page.evaluate((cfg) => {
const { Graph, GraphCanvas, GraphBlock, React, ReactDOM } = (window as any).GraphModule;

const rootEl = document.getElementById("root");
if (!rootEl) {
throw new Error("Root element not found");
}

const graph = new Graph(cfg, rootEl);

// Render with React and GraphCanvas (enables HTML block rendering via BlocksList)
const reactRoot = ReactDOM.createRoot(rootEl);

const renderBlock = (g: unknown, block: { id: string; name?: string }) => {
return React.createElement(
GraphBlock,
{ graph: g, block },
React.createElement("div", { "data-testid": `block-${block.id}`, style: { padding: "8px" } }, block.name || block.id)
);
};

reactRoot.render(
React.createElement(GraphCanvas, {
graph,
renderBlock,
style: { width: "100%", height: "100vh" },
})
);

// Set initial entities if provided
if (cfg.blocks || cfg.connections) {
graph.setEntities({
blocks: cfg.blocks || [],
connections: cfg.connections || [],
});
}

graph.start();
graph.zoomTo("center");

// Expose to window for tests
window.graph = graph;
window.graphInitialized = true;
}, config);
}

/**
* Get count of rendered HTML blocks in the DOM
*/
async getRenderedHtmlBlockCount(): Promise<number> {
return await this.page.evaluate(() => {
return document.querySelectorAll("[data-testid^='block-']").length;
});
}

/**
* Get IDs of rendered HTML blocks in the DOM
*/
async getRenderedHtmlBlockIds(): Promise<string[]> {
return await this.page.evaluate(() => {
const elements = document.querySelectorAll("[data-testid^='block-']");
return Array.from(elements).map((el) =>
el.getAttribute("data-testid")?.replace("block-", "") || ""
);
});
}

/**
* Check if a specific HTML block is rendered in the DOM
*/
async isHtmlBlockRendered(blockId: string): Promise<boolean> {
return await this.page.evaluate((id) => {
return !!document.querySelector(`[data-testid='block-${id}']`);
}, blockId);
}
}
26 changes: 26 additions & 0 deletions e2e/pages/react.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Graph React E2E Test</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="/e2e/dist/graph-react.bundle.js"></script>
<script>
// Bundle exposes GraphModule global
window.graphLibraryLoaded = true;
</script>
</body>
</html>
13 changes: 13 additions & 0 deletions e2e/react-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// React E2E bundle entry point
import React from "react";
import ReactDOM from "react-dom/client";

import "../src/services/Layer.css";
import "../src/react-components/graph-canvas.css";
import "../src/react-components/Block.css";
import "../src/react-components/Anchor.css";

// Re-export everything from main and react indexes
export * from "../src/index";
export * from "../src/react-components/index";
export { React, ReactDOM };
22 changes: 22 additions & 0 deletions e2e/tests/reload-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from "@playwright/test";
import { ReactGraphPageObject } from "../page-objects/ReactGraphPageObject";
import { TBlock } from "../../src/components/canvas/blocks/Block";

const blocks: TBlock[] = [
{ id: "b1", is: "Block", x: 100, y: 100, width: 200, height: 100, name: "B1", anchors: [], selected: false },
{ id: "b2", is: "Block", x: 400, y: 100, width: 200, height: 100, name: "B2", anchors: [], selected: false },
];

test("Issue #249: html blocks disappear after reload with double setEntities", async ({ page }) => {
const graphPage = new ReactGraphPageObject(page);
await graphPage.initialize({ blocks, connections: [] });
await graphPage.setZoom(1.0);
await graphPage.waitForFrames(10);

// Simulate "reload" — double setEntities
await graphPage.setEntities({ blocks: [], connections: [] });
await graphPage.setEntities({ blocks, connections: [] });
await graphPage.waitForFrames(20);

expect(await graphPage.getRenderedHtmlBlockCount()).toBe(2);
});
8 changes: 1 addition & 7 deletions e2e/tests/selection-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,10 @@ test.describe("Selection Test", () => {
// Try selecting programmatically first
await page.evaluate(() => {
const blockState = window.graph.blocks.getBlockState("block-1");
console.log("BlockState exists:", !!blockState);
console.log("SelectionService:", window.graph.selectionService);

// Try to select the block using correct API
const { ESelectionStrategy } = window.GraphModule;
window.graph.selectionService.select(
"block",
["block-1"],
ESelectionStrategy.REPLACE
);
window.graph.selectionService.select("block", ["block-1"], ESelectionStrategy.REPLACE);
});

await page.waitForTimeout(200);
Expand Down
Loading
Loading