Files
diodeinc__pcb/vscode/preview/tests/visual/LayoutEngine.visual.pw.tsx
Lenny Khazan acff75845c Schematics Viewer v2 (#23)
* Support net symbols

* Run renderer tests in CI

* renderer -> LayoutEngine

* wip

* rename

* add examples

* support symbol "extends"

* start reworking layout engine + snapshot tests

* add examples

* wip add nets

* Add signature to ModuleValue

* tweaks

* per-component net labels

* movable

* WIP with ELK

* wip libavoid

* misc fix

* better

* betterer

* improvements

* more

* update

* junction detection

* better

* snapping and stuff

* fix re-rendering

* more better

* more

* cargo fmt

* update bg

* eslint

* cleanup, tests
2025-07-12 13:24:55 -04:00

305 lines
8.9 KiB
TypeScript

import { test, expect } from "@playwright/experimental-ct-react";
import React from "react";
import SchematicContainer from "../../src/components/SchematicContainer";
import type { Netlist } from "../../src/types/NetlistTypes";
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
test.describe("Schematic Visual Tests", () => {
const examplesDir = path.join(__dirname, "../../../../examples");
// Helper function to build netlist from example
async function buildNetlistFromExample(
exampleName: string
): Promise<Netlist> {
const examplePath = path.join(examplesDir, exampleName);
const starFile = path.join(examplePath, `${exampleName}.zen`);
if (!fs.existsSync(starFile)) {
throw new Error(`Example file not found: ${starFile}`);
}
// Run cargo to build netlist
try {
const output = execSync(`cargo run -- build --netlist "${starFile}"`, {
cwd: path.join(__dirname, "../../../../"),
stdio: "pipe",
encoding: "utf-8",
});
// Parse the output directly from stdout
return JSON.parse(output) as Netlist;
} catch (error: any) {
console.error(
`Failed to build netlist for ${exampleName}:`,
error.stderr?.toString()
);
throw error;
}
}
// Get all example directories
const exampleDirs = fs
.readdirSync(examplesDir)
.filter((name) => fs.statSync(path.join(examplesDir, name)).isDirectory());
// Dynamically generate a test for each example
for (const exampleName of exampleDirs) {
test(`${exampleName}`, async ({ mount, page }) => {
// Capture console logs from the browser
page.on("console", async (msg) => {
const type = msg.type();
// Get all arguments passed to console.log/error/etc
const args: string[] = [];
for (const arg of msg.args()) {
args.push(arg.toString());
}
if (msg.text().includes("kicanvas")) {
return;
}
// Log to Node.js console with appropriate formatting
if (type === "log") {
console.log("[Browser Console]", ...args);
} else if (type === "error") {
console.error("[Browser Console Error]", ...args);
} else if (type === "warning") {
console.warn("[Browser Console Warning]", ...args);
} else if (type === "info") {
console.info("[Browser Console Info]", ...args);
}
});
// Also capture page errors
page.on("pageerror", (error) => {
console.error(`[Page Error] ${error.message}`);
console.error(error.stack);
});
const netlist = await buildNetlistFromExample(exampleName);
// Mount the component
const component = await mount(
<>
<style>{`
/* Ensure the mount root has full height */
#root {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
/* Override the 100vh in SchematicContainer to use parent height */
.schematic-layout {
height: 100% !important;
}
.schematic-viewer-container {
height: 100% !important;
}
/* Ensure React Flow fills the container */
.react-flow {
height: 100% !important;
width: 100% !important;
}
.react-flow__renderer {
height: 100% !important;
width: 100% !important;
}
.react-flow__viewport {
height: 100% !important;
width: 100% !important;
}
.schematic-viewer {
height: 100% !important;
width: 100% !important;
}
.react-flow-schematic-viewer {
height: 100% !important;
width: 100% !important;
}
`}</style>
<SchematicContainer
netlistData={netlist}
currentFile={netlist.root_ref.split(":")[0]}
selectedModule={netlist.root_ref}
/>
</>
);
// Wait for React Flow to finish rendering
await page.waitForSelector(".react-flow__renderer", {
state: "attached",
timeout: 10000,
});
// Wait for nodes to be rendered
await page.waitForSelector(".react-flow__node", {
state: "visible",
timeout: 10000,
});
// Give time for layout to stabilize and animations to complete
await page.waitForTimeout(1000);
// Check for any error messages
const errorElement = await page.$(".error-message");
if (errorElement) {
const errorText = await errorElement.textContent();
throw new Error(`Error message found in ${exampleName}: ${errorText}`);
}
// Take screenshot
await expect(component).toHaveScreenshot(
`${exampleName.toLowerCase()}.png`,
{
animations: "disabled",
scale: "css",
}
);
});
}
// Test rotation functionality
test("VoltageDivider with rotation", async ({ mount, page }) => {
// Capture console logs from the browser
page.on("console", async (msg) => {
const type = msg.type();
const args: string[] = [];
for (const arg of msg.args()) {
args.push(arg.toString());
}
if (msg.text().includes("kicanvas")) {
return;
}
if (type === "log") {
console.log("[Browser Console]", ...args);
} else if (type === "error") {
console.error("[Browser Console Error]", ...args);
}
});
page.on("pageerror", (error) => {
console.error(`[Page Error] ${error.message}`);
console.error(error.stack);
});
const netlist = await buildNetlistFromExample("VoltageDivider");
// Mount the component
const component = await mount(
<>
<style>{`
/* Ensure the mount root has full height */
#root {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
/* Override the 100vh in SchematicContainer to use parent height */
.schematic-layout {
height: 100% !important;
}
.schematic-viewer-container {
height: 100% !important;
}
/* Ensure React Flow fills the container */
.react-flow {
height: 100% !important;
width: 100% !important;
}
.react-flow__renderer {
height: 100% !important;
width: 100% !important;
}
.react-flow__viewport {
height: 100% !important;
width: 100% !important;
}
.schematic-viewer {
height: 100% !important;
width: 100% !important;
}
.react-flow-schematic-viewer {
height: 100% !important;
width: 100% !important;
}
`}</style>
<SchematicContainer
netlistData={netlist}
currentFile={netlist.root_ref.split(":")[0]}
selectedModule={netlist.root_ref}
/>
</>
);
// Wait for React Flow to finish rendering
await page.waitForSelector(".react-flow__renderer", {
state: "attached",
timeout: 10000,
});
// Wait for nodes to be rendered
await page.waitForSelector(".react-flow__node", {
state: "visible",
timeout: 10000,
});
// Give time for layout to stabilize
await page.waitForTimeout(1000);
// Find and click on a resistor node (R1 or R2)
const resistorNode = await page
.locator('.react-flow__node[data-id*="R1"]')
.first();
// Click to select the resistor
await resistorNode.click();
// Wait a bit for selection to register
await page.waitForTimeout(100);
// Press 'R' to rotate
await page.keyboard.press("r");
// Wait for rotation animation and re-layout
await page.waitForTimeout(500);
// Take screenshot with rotated component
await expect(component).toHaveScreenshot("voltagedivider-rotated.png", {
animations: "disabled",
scale: "css",
});
// Rotate again (should be 180 degrees now)
await page.keyboard.press("r");
await page.waitForTimeout(500);
await expect(component).toHaveScreenshot("voltagedivider-rotated-180.png", {
animations: "disabled",
scale: "css",
});
// Rotate again (should be 270 degrees now)
await page.keyboard.press("r");
await page.waitForTimeout(500);
await expect(component).toHaveScreenshot("voltagedivider-rotated-270.png", {
animations: "disabled",
scale: "css",
});
// Rotate once more (should be back to 0 degrees)
await page.keyboard.press("r");
await page.waitForTimeout(500);
await expect(component).toHaveScreenshot("voltagedivider-rotated-360.png", {
animations: "disabled",
scale: "css",
});
});
});