graph-explorer
Version:
Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.
180 lines (165 loc) • 5.62 kB
text/typescript
import type { Page } from "@playwright/test";
export interface PageProblems {
consoleErrors: string[];
pageErrors: string[];
failedImages: string[];
}
const isSameOrigin = (url: string) =>
url.startsWith("http://localhost:") || url.startsWith("/");
/**
* Starts collecting console/page errors. Must be called before navigating.
* Third-party resources (e.g. the font-awesome CDN used by the demo pages) are
* ignored so the assertions stay stable on flaky networks.
*/
export function collectPageProblems(page: Page): PageProblems {
const problems: PageProblems = {
consoleErrors: [],
pageErrors: [],
failedImages: [],
};
page.on("console", (message) => {
if (message.type() !== "error") {
return;
}
const location = message.location()?.url ?? "";
if (location && !isSameOrigin(location)) {
return;
}
problems.consoleErrors.push(message.text());
});
page.on("pageerror", (error) => problems.pageErrors.push(error.message));
page.on("response", (response) => {
const url = response.url();
if (!isSameOrigin(url) || response.status() < 400) {
return;
}
if (/\.(svg|png|jpe?g|gif)(\?|$)/i.test(url)) {
problems.failedImages.push(`${response.status()} ${url}`);
}
});
return problems;
}
/** Opens the bundled local demo (deterministic, no network data source). */
export async function openLocalDemo(page: Page): Promise<void> {
await page.goto("/", { waitUntil: "load" });
await page.waitForSelector(".graph-explorer-class-leaf");
}
/** Drags the first available instance onto the canvas to create an element. */
export async function createElementOnCanvas(
page: Page,
targetPosition = { x: 500, y: 300 }
): Promise<void> {
await page.locator(".graph-explorer-class-leaf__body").first().click();
await page.waitForSelector(".graph-explorer-list-element-view");
const created = page.locator(".graph-explorer-overlayed-element").first();
// the browser occasionally drops the simulated HTML5 drag, so retry
for (let attempt = 0; attempt < 3; attempt++) {
await page
.locator(".graph-explorer-list-element-view")
.first()
.dragTo(page.locator(".graph-explorer-paper-area").first(), {
targetPosition,
force: true,
});
try {
await created.waitFor({ state: "visible", timeout: 5_000 });
return;
} catch {
// fall through and try the drag again
}
}
throw new Error("could not create an element on the canvas after 3 attempts");
}
/**
* The paper area adjusts and re-centres its scroll position asynchronously
* after mounting (and after content changes). Panning before that settles gets
* overwritten, so wait until the scroll offsets stop moving.
*/
export async function waitForCanvasSettled(page: Page): Promise<void> {
await page.evaluate(() => {
delete (window as unknown as Record<string, unknown>).__geScroll;
});
await page.waitForFunction(
() => {
const area = document.querySelector(".graph-explorer-paper-area__area");
if (!area) {
return false;
}
const store = window as unknown as {
__geScroll?: { left: number; top: number; stable: number };
};
const previous = store.__geScroll;
const current = {
left: area.scrollLeft,
top: area.scrollTop,
stable: 0,
};
if (
previous &&
previous.left === current.left &&
previous.top === current.top
) {
current.stable = previous.stable + 1;
}
store.__geScroll = current;
// stay stable long enough to outlast the mount/centring animation
return current.stable >= 5;
},
undefined,
{ timeout: 15_000, polling: 150 }
);
}
export interface ElementPlacement {
/** position on screen, in viewport pixels */
left: number;
top: number;
width: number;
height: number;
/** inline transform, i.e. the element's position in paper coordinates */
transform: string;
}
/**
* Where the first diagram element sits, both on screen and within the diagram.
*
* Panning is asserted through this rather than through `scrollLeft`/`scrollTop`:
* `PaperArea` re-measures the paper on content/viewport changes and rewrites the
* scroll offsets (see its `componentDidUpdate`) to keep content visually in
* place, so the raw scroll numbers can jump at any time. The on-screen position
* is the value that is actually preserved, and the paper-space `transform`
* distinguishes "the canvas moved" from "the element was dragged".
*/
export async function elementPlacement(page: Page): Promise<ElementPlacement> {
const element = page.locator(".graph-explorer-overlayed-element").first();
const box = await element.boundingBox();
if (!box) {
throw new Error("no diagram element is visible on the canvas");
}
const transform = await element.evaluate(
(node) => (node as HTMLElement).style.transform
);
return {
left: box.x,
top: box.y,
width: box.width,
height: box.height,
transform,
};
}
/** Presses the given mouse button and drags in steps so move handlers run. */
export async function dragWithButton(
page: Page,
button: "left" | "middle",
from: { x: number; y: number },
delta: { x: number; y: number }
): Promise<void> {
await page.mouse.move(from.x, from.y);
await page.mouse.down({ button });
const steps = 5;
for (let step = 1; step <= steps; step++) {
await page.mouse.move(
from.x + (delta.x * step) / steps,
from.y + (delta.y * step) / steps
);
}
await page.mouse.up({ button });
}