diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 0000000..fe26a9c
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,33 @@
+name: Playwright Tests
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+jobs:
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ timeout-minutes: 5
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: boost
+ if: startsWith(matrix.os, 'ubuntu')
+ run: sudo apt-get update && sudo apt-get install -yq libboost-dev
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ - name: Install dependencies
+ run: npm ci
+ - name: Build
+ run: npm run build
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps chromium
+ - name: Run Playwright tests
+ run: xvfb-run npm test
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
index a8b0f37..dc6a8ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,14 @@
/bin
/minkowski
/deepnest-*win32-x64
-package-lock.json
+/output
+# package-lock.json
git_token.rtf
.idea
*.zip
.python-version
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/package.json b/package.json
index 3506a0d..9fe361c 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "1.2.2",
"description": "Deep nesting for Laser and CNC",
"main": "main.js",
+ "types": "index.d.ts",
"license": "MIT",
"scripts": {
"start": "electron .",
diff --git a/tests/assets/henny-penny.svg b/tests/assets/henny-penny.svg
new file mode 100644
index 0000000..870eb9d
--- /dev/null
+++ b/tests/assets/henny-penny.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/assets/mrs-saint-delafield.svg b/tests/assets/mrs-saint-delafield.svg
new file mode 100644
index 0000000..650d2bd
--- /dev/null
+++ b/tests/assets/mrs-saint-delafield.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/index.spec.ts b/tests/index.spec.ts
new file mode 100644
index 0000000..ff966bd
--- /dev/null
+++ b/tests/index.spec.ts
@@ -0,0 +1,154 @@
+import {
+ type ConsoleMessage,
+ _electron as electron,
+ expect,
+ test,
+} from "@playwright/test";
+import { OpenDialogReturnValue } from "electron";
+import { readdir, readFile } from "fs/promises";
+import path from "path";
+import { fileURLToPath } from "url";
+import { DeepNestConfig, NestingResult } from "../index";
+
+// !process.env.CI && test.use({ launchOptions: { slowMo: !process.env.CI ? 500 : 0 } });
+
+const sheet = { width: 300, height: 200 };
+
+test("Nest", async ({}, testInfo) => {
+ const electronApp = await electron.launch({
+ args: ["main.js"],
+ recordVideo: { dir: testInfo.outputDir },
+ });
+
+ const window = await electronApp.firstWindow();
+
+ // // Pipe Electron console to Node terminal.
+ // const logMessage = async (message: ConsoleMessage) => {
+ // const { url, lineNumber, columnNumber } = message.location();
+ // let file = url;
+ // try {
+ // file = path.relative(process.cwd(), fileURLToPath(url));
+ // } catch (error) {}
+ // console.log({
+ // location: `${file}:${lineNumber}:${columnNumber}`,
+ // args: await Promise.all(message.args().map((x) => x.jsonValue())),
+ // type: message.type(),
+ // });
+ // };
+ // window.on("console", logMessage);
+ // electronApp.on("window", (win) => win.on("console", logMessage));
+
+ await test.step("upload and start", async () => {
+ const inputDir = path.resolve(__dirname, "assets");
+ const files = (await readdir(inputDir))
+ .filter((file) => path.extname(file) === ".svg")
+ .map((file) => path.resolve(inputDir, file));
+
+ await electronApp.evaluate(({ dialog }, paths) => {
+ dialog.showOpenDialog = async (): Promise => ({
+ filePaths: paths,
+ canceled: false,
+ });
+ }, files);
+ await window.click("id=import");
+
+ await window.click("id=addsheet");
+ await window.fill("id=sheetwidth", sheet.width.toString());
+ await window.fill("id=sheetheight", sheet.height.toString());
+ await window.click("id=confirmsheet");
+
+ const spacingMM = 10;
+ const scale = 72;
+ const config: DeepNestConfig = {
+ units: "mm",
+ scale, // stored value will be in units/inch
+ spacing: (spacingMM / 25.4) * scale, // stored value will be in units/inch
+ curveTolerance: 0.72, // store distances in native units
+ clipperScale: 10000000,
+ rotations: 4,
+ threads: 4,
+ populationSize: 10,
+ mutationRate: 10,
+ placementType: "gravity", // how to place each part (possible values gravity, box, convexhull)
+ mergeLines: true, // whether to merge lines
+ timeRatio: 0.5, // ratio of material reduction to laser time. 0 = optimize material only, 1 = optimize laser time only
+ simplify: false,
+ dxfImportScale: 1,
+ dxfExportScale: 72,
+ endpointTolerance: 0.36,
+ conversionServer: "http://convert.deepnest.io",
+ };
+
+ await window.evaluate((config) => {
+ window.config.setSync(config);
+ window.DeepNest.config(config);
+ }, config);
+
+ // await expect(window).toHaveScreenshot("loaded.png", {
+ // clip: { x: 100, y: 100, width: 2000, height: 1000 },
+ // });
+
+ await window.click("id=startnest");
+ });
+
+ const stopNesting = () => window.click("id=stopnest");
+
+ const downloadSvg = async () => {
+ const file = testInfo.outputPath("output.svg");
+ electronApp.evaluate(({ dialog }, path) => {
+ dialog.showSaveDialogSync = () => path;
+ }, file);
+ await window.click("id=export");
+ await expect(window.locator("id=exportsvg")).toBeVisible();
+ await window.click("id=exportsvg");
+ return (await readFile(file)).toString();
+ };
+
+ const waitForIteration = (n: number) =>
+ expect(() =>
+ expect(
+ window
+ .locator("id=nestlist")
+ .locator("span")
+ .nth(n - 1)
+ ).toBeVisible()
+ ).toPass();
+
+ await expect(window.locator("id=progressbar")).toBeVisible();
+ await waitForIteration(1);
+ await expect(window.locator("id=nestinfo").locator("h1").nth(0)).toHaveText(
+ "1"
+ );
+ await expect(window.locator("id=nestinfo").locator("h1").nth(1)).toHaveText(
+ "54/54"
+ );
+
+ const svg = await downloadSvg();
+
+ const data = (): Promise =>
+ window.evaluate(() => window.DeepNest.nests);
+
+ testInfo.attach("nesting.svg", { body: svg, contentType: "image/svg+xml" });
+
+ testInfo.attach("nesting.json", {
+ body: JSON.stringify(await data(), null, 2),
+ contentType: "application/json",
+ });
+
+ await stopNesting();
+
+ await electronApp.close();
+});
+
+test.afterAll(async ({}, testInfo) => {
+ const { outputDir } = testInfo;
+ await Promise.all(
+ (
+ await readdir(outputDir)
+ ).map((file) => {
+ return testInfo.attach(file, {
+ path: path.resolve(outputDir, file),
+ });
+ })
+ );
+});