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), + }); + }) + ); +});