From 128c9cb3f8dd5275f8cd68bdf1b23faf028cf9ff Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Tue, 13 Aug 2024 17:00:56 +1000 Subject: [PATCH] Example electron test (#3408) * example test mostly working * add electron test to CI --- .github/workflows/playwright.yml | 161 ++++++++++++++++++++++++++++++- .gitignore | 5 +- e2e/playwright/projects.spec.ts | 130 ++++++++++++++++++------- src/lib/desktop.ts | 9 ++ src/main.ts | 20 +++- 5 files changed, 282 insertions(+), 43 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6738f109c..e10745900 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -171,7 +171,7 @@ jobs: if [[ ! -f "test-results/.last-run.json" ]]; then # if no last run artifact, than run plawright normally echo "run playwright normally" - yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true + yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert='@(snapshot|electron)' || true # # send to axiom node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 fi @@ -186,7 +186,7 @@ jobs: if [[ $failed_tests -gt 0 ]]; then echo "retried=true" >>$GITHUB_OUTPUT echo "run playwright with last failed tests and retry $retry" - yarn playwright test --project="Google Chrome" --last-failed --grep-invert=@snapshot || true + yarn playwright test --project="Google Chrome" --last-failed --grep-invert='@(snapshot|electron)' || true # send to axiom node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 retry=$((retry + 1)) @@ -233,6 +233,159 @@ jobs: retention-days: 30 overwrite: true + playwright-electron: + timeout-minutes: 30 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shardIndex: [1] + shardTotal: [1] + needs: check-rust-changes + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - uses: KittyCAD/action-install-cli@main + - name: Install dependencies + run: yarn + - name: Cache Playwright Browsers + uses: actions/cache@v4 + with: + path: | + ~/.cache/ms-playwright/ + key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }} + - name: Install Playwright Browsers + run: yarn playwright install chromium --with-deps + # run: yarn playwright install --with-deps + - name: Download Wasm Cache + id: download-wasm + if: needs.check-rust-changes.outputs.rust-changed == 'false' + uses: dawidd6/action-download-artifact@v6 + continue-on-error: true + with: + github_token: ${{secrets.GITHUB_TOKEN}} + name: wasm-bundle + workflow: build-and-store-wasm.yml + branch: main + path: src/wasm-lib/pkg + - name: copy wasm blob + if: needs.check-rust-changes.outputs.rust-changed == 'false' + run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public + continue-on-error: true + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + - name: Cache Wasm (because rust diff) + if: needs.check-rust-changes.outputs.rust-changed == 'true' + uses: Swatinem/rust-cache@v2 + with: + workspaces: './src/wasm-lib' + - name: OR Cache Wasm (because wasm cache failed) + if: steps.download-wasm.outcome == 'failure' + uses: Swatinem/rust-cache@v2 + with: + workspaces: './src/wasm-lib' + - name: Install vector + run: | + curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh + chmod +x /tmp/vector.sh + /tmp/vector.sh -y -no-modify-path + mkdir -p /tmp/vector + cp .github/workflows/vector.toml /tmp/vector.toml + sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml + sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml + sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml + sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml + sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml + cat /tmp/vector.toml + ${HOME}/.vector/bin/vector --config /tmp/vector.toml & + - name: Build Wasm (because rust diff) + if: needs.check-rust-changes.outputs.rust-changed == 'true' + run: yarn build:wasm + - name: OR Build Wasm (because wasm cache failed) + if: steps.download-wasm.outcome == 'failure' + run: yarn build:wasm + - name: build web + run: yarn build:local + - uses: actions/download-artifact@v4 + if: always() + continue-on-error: true + with: + name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} + path: test-results/ + - name: Run ubuntu/chrome flow (with retries) + id: retry + if: always() + run: | + if [[ ! -f "test-results/.last-run.json" ]]; then + # if no last run artifact, than run plawright normally + echo "run playwright normally" + yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep=@electron || true + # # send to axiom + node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 + fi + + retry=1 + max_retrys=4 + + # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues + while [[ $retry -le $max_retrys ]]; do + if [[ -f "test-results/.last-run.json" ]]; then + failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) + if [[ $failed_tests -gt 0 ]]; then + echo "retried=true" >>$GITHUB_OUTPUT + echo "run playwright with last failed tests and retry $retry" + yarn playwright test --project="Google Chrome" --last-failed --grep=@electron || true + # send to axiom + node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 + retry=$((retry + 1)) + else + echo "retried=false" >>$GITHUB_OUTPUT + exit 0 + fi + else + echo "retried=false" >>$GITHUB_OUTPUT + exit 0 + fi + done + + echo "retried=false" >>$GITHUB_OUTPUT + + if [[ -f "test-results/.last-run.json" ]]; then + failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) + if [[ $failed_tests -gt 0 ]]; then + # if it still fails after 3 retrys, then fail the job + exit 1 + fi + fi + exit 0 + env: + CI: true + token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} + - name: send to axiom + if: always() + shell: bash + run: | + node playwrightProcess.mjs | tee /tmp/github-actions.log + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-electron-${{ matrix.shardIndex }}-${{ github.sha }} + path: test-results/ + retention-days: 30 + overwrite: true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-electron-${{ matrix.shardIndex }}-${{ github.sha }} + path: playwright-report/ + retention-days: 30 + overwrite: true + playwright-macos: timeout-minutes: 30 runs-on: macos-14 @@ -325,7 +478,7 @@ jobs: if [[ ! -f "test-results/.last-run.json" ]]; then # if no last run artifact, than run plawright normally echo "run playwright normally" - yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true + yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert='@(snapshot|electron)' || true # # send to axiom node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 fi @@ -340,7 +493,7 @@ jobs: if [[ $failed_tests -gt 0 ]]; then echo "retried=true" >>$GITHUB_OUTPUT echo "run playwright with last failed tests and retry $retry" - yarn playwright test --project="webkit" --last-failed --grep-invert=@snapshot || true + yarn playwright test --project="webkit" --last-failed --grep-invert='@(snapshot|electron)' || true # send to axiom node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 retry=$((retry + 1)) diff --git a/.gitignore b/.gitignore index fcd9199b1..28f97c4de 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,7 @@ venv .vite/ # electron -out/ \ No newline at end of file +out/ + +src-tauri/target +electron-test-projects-dir \ No newline at end of file diff --git a/e2e/playwright/projects.spec.ts b/e2e/playwright/projects.spec.ts index 583add40b..ddb78aaa2 100644 --- a/e2e/playwright/projects.spec.ts +++ b/e2e/playwright/projects.spec.ts @@ -1,40 +1,102 @@ -import { _electron as electron, test } from '@playwright/test'; +import { _electron as electron, test, expect } from '@playwright/test' import { getUtils, - TEST_COLORS, - setup, tearDown, - commonPoints, - PERSIST_MODELING_CONTEXT, } from './test-utils' +import fs from 'node:fs' +import { secrets } from './secrets' -test.describe("when a project", async () => { - - // This was the very test created. It provides the foundation for the - // rest of the tests we have to write. - test('is created', async ({ page }) => { - // Launch Electron app. - const electronApp = await electron.launch({ args: ['.'] }) - - // Evaluation expression in the Electron context. - const appPath = await electronApp.evaluate(async ({ app }) => { - // This runs in the main Electron process, parameter here is always - // the result of the require('electron') in the main app script. - return app.getAppPath(); - }); - console.log(appPath); - - // Get the first window that the app opens, wait if necessary. - const window = await electronApp.firstWindow(); - // Print the title. - console.log(await window.title()); - // Capture a screenshot. - await window.screenshot({ path: 'intro.png' }); - // Direct Electron console to Node terminal. - window.on('console', console.log); - // Click button. - await window.click('text=Click me'); - // Exit app. - await electronApp.close(); - }) +test.afterEach(async ({ page }, testInfo) => { + await tearDown(page, testInfo) +}) + +test('When the project folder is empty, user can create new project and open it.', { tag: '@electron' }, async () => { + // create or otherwise clear the folder ./electron-test-projects-dir + const fileName = './electron-test-projects-dir' + try { + fs.rmdirSync(fileName, { recursive: true }) + } catch (e) { + console.error(e) + } + + fs.mkdirSync(fileName) + + // get full path for ./electron-test-projects-dir + const fullPath = fs.realpathSync(fileName) + + const electronApp = await electron.launch({ + args: ['.'], + }) + + await electronApp.evaluate(async ({ app }) => { + return app.getAppPath() + }) + + const page = await electronApp.firstWindow() + + // Set local storage directly using evaluate + await page.evaluate( + (token) => localStorage.setItem('TOKEN_PERSIST_KEY', token), + secrets.token + ) + await page.evaluate((fullPath) => + localStorage.setItem( + 'APP_SETTINGS_OVERRIDE', + JSON.stringify({ + projectDirectory: fullPath, + }) + ), fullPath + ) + + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + + page.on('console', console.log) + + // expect to see text "No Projects found" + await expect(page.getByText('No Projects found')).toBeVisible() + + await page.getByRole('button', { name: 'New project' }).click() + + await expect(page.getByText('Successfully created')).toBeVisible() + await expect(page.getByText('Successfully created')).not.toBeVisible() + + await expect(page.getByText('project-000')).toBeVisible() + + await page.getByText('project-000').click() + + await expect(page.getByTestId('loading')).toBeAttached() + await expect(page.getByTestId('loading')).not.toBeAttached({ + timeout: 20_000, + }) + + await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({ + timeout: 20_000, + }) + + await page.locator('.cm-content').fill(`const sketch001 = startSketchOn('XZ') + |> startProfileAt([-87.4, 282.92], %) + |> line([324.07, 27.199], %, $seg01) + |> line([118.328, -291.754], %) + |> line([-180.04, -202.08], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(200, sketch001)`) + + const pointOnModel = { x: 660, y: 250 } + + // check the model loaded by checking it's grey + await expect + .poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), { + timeout: 10_000, + }) + .toBeLessThan(10) + + await page.mouse.click(pointOnModel.x, pointOnModel.y) + // check user can interact with model by checking it turns yellow + await expect + .poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132])) + .toBeLessThan(10) + + await electronApp.close() }) diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index b76f38177..61e809f00 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -441,6 +441,15 @@ export const readAppSettingsFile = async () => { } const configToml = await window.electron.readFile(settingsPath) const configObj = parseAppSettings(configToml) + const overrideJSON = localStorage.getItem('APP_SETTINGS_OVERRIDE') + if (overrideJSON) { + try { + const override = JSON.parse(overrideJSON) + configObj.app = { ...configObj.app, ...override } + } catch (e) { + console.error('Error parsing APP_SETTINGS_OVERRIDE:', e) + } + } return configObj } diff --git a/src/main.ts b/src/main.ts index c1b362abc..e4e63de03 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,15 @@ // template that ElectronJS provides. import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' -import { app, BrowserWindow, ipcMain, dialog, shell, protocol, net } from 'electron' +import { + app, + BrowserWindow, + ipcMain, + dialog, + shell, + protocol, + net, +} from 'electron' import path from 'path' import url from 'url' import fs from 'node:fs/promises' @@ -41,7 +49,7 @@ const createWindow = () => { // Open the DevTools. // mainWindow.webContents.openDevTools() - mainWindow.show() + mainWindow.show() } // Quit when all windows are closed, except on macOS. There, it's common @@ -153,8 +161,12 @@ app.whenReady().then(() => { const maybeAbsolutePath = path.join(__dirname, filePath) const bypassCustomProtocolHandlers = true if (fss.existsSync(maybeAbsolutePath)) { - console.log(`Intercepted local-asbolute path ${filePath}, rebuilt it as ${maybeAbsolutePath}`) - return net.fetch(url.pathToFileURL(maybeAbsolutePath).toString(), { bypassCustomProtocolHandlers }) + console.log( + `Intercepted local-asbolute path ${filePath}, rebuilt it as ${maybeAbsolutePath}` + ) + return net.fetch(url.pathToFileURL(maybeAbsolutePath).toString(), { + bypassCustomProtocolHandlers, + }) } console.log(`Default fetch to ${filePath}`) return net.fetch(request.url, { bypassCustomProtocolHandlers })