Example electron test (#3408)
* example test mostly working * add electron test to CI
This commit is contained in:
161
.github/workflows/playwright.yml
vendored
161
.github/workflows/playwright.yml
vendored
@ -171,7 +171,7 @@ jobs:
|
|||||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||||
# if no last run artifact, than run plawright normally
|
# if no last run artifact, than run plawright normally
|
||||||
echo "run playwright 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
|
# # send to axiom
|
||||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
@ -186,7 +186,7 @@ jobs:
|
|||||||
if [[ $failed_tests -gt 0 ]]; then
|
if [[ $failed_tests -gt 0 ]]; then
|
||||||
echo "retried=true" >>$GITHUB_OUTPUT
|
echo "retried=true" >>$GITHUB_OUTPUT
|
||||||
echo "run playwright with last failed tests and retry $retry"
|
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
|
# send to axiom
|
||||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||||
retry=$((retry + 1))
|
retry=$((retry + 1))
|
||||||
@ -233,6 +233,159 @@ jobs:
|
|||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
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:
|
playwright-macos:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
@ -325,7 +478,7 @@ jobs:
|
|||||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||||
# if no last run artifact, than run plawright normally
|
# if no last run artifact, than run plawright normally
|
||||||
echo "run playwright 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
|
# # send to axiom
|
||||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
@ -340,7 +493,7 @@ jobs:
|
|||||||
if [[ $failed_tests -gt 0 ]]; then
|
if [[ $failed_tests -gt 0 ]]; then
|
||||||
echo "retried=true" >>$GITHUB_OUTPUT
|
echo "retried=true" >>$GITHUB_OUTPUT
|
||||||
echo "run playwright with last failed tests and retry $retry"
|
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
|
# send to axiom
|
||||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||||
retry=$((retry + 1))
|
retry=$((retry + 1))
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -65,4 +65,7 @@ venv
|
|||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
# electron
|
# electron
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
src-tauri/target
|
||||||
|
electron-test-projects-dir
|
||||||
@ -1,40 +1,102 @@
|
|||||||
import { _electron as electron, test } from '@playwright/test';
|
import { _electron as electron, test, expect } from '@playwright/test'
|
||||||
import {
|
import {
|
||||||
getUtils,
|
getUtils,
|
||||||
TEST_COLORS,
|
|
||||||
setup,
|
|
||||||
tearDown,
|
tearDown,
|
||||||
commonPoints,
|
|
||||||
PERSIST_MODELING_CONTEXT,
|
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import { secrets } from './secrets'
|
||||||
|
|
||||||
test.describe("when a project", async () => {
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
|
await tearDown(page, testInfo)
|
||||||
// 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 }) => {
|
test('When the project folder is empty, user can create new project and open it.', { tag: '@electron' }, async () => {
|
||||||
// Launch Electron app.
|
// create or otherwise clear the folder ./electron-test-projects-dir
|
||||||
const electronApp = await electron.launch({ args: ['.'] })
|
const fileName = './electron-test-projects-dir'
|
||||||
|
try {
|
||||||
// Evaluation expression in the Electron context.
|
fs.rmdirSync(fileName, { recursive: true })
|
||||||
const appPath = await electronApp.evaluate(async ({ app }) => {
|
} catch (e) {
|
||||||
// This runs in the main Electron process, parameter here is always
|
console.error(e)
|
||||||
// the result of the require('electron') in the main app script.
|
}
|
||||||
return app.getAppPath();
|
|
||||||
});
|
fs.mkdirSync(fileName)
|
||||||
console.log(appPath);
|
|
||||||
|
// get full path for ./electron-test-projects-dir
|
||||||
// Get the first window that the app opens, wait if necessary.
|
const fullPath = fs.realpathSync(fileName)
|
||||||
const window = await electronApp.firstWindow();
|
|
||||||
// Print the title.
|
const electronApp = await electron.launch({
|
||||||
console.log(await window.title());
|
args: ['.'],
|
||||||
// Capture a screenshot.
|
})
|
||||||
await window.screenshot({ path: 'intro.png' });
|
|
||||||
// Direct Electron console to Node terminal.
|
await electronApp.evaluate(async ({ app }) => {
|
||||||
window.on('console', console.log);
|
return app.getAppPath()
|
||||||
// Click button.
|
})
|
||||||
await window.click('text=Click me');
|
|
||||||
// Exit app.
|
const page = await electronApp.firstWindow()
|
||||||
await electronApp.close();
|
|
||||||
})
|
// 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()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -441,6 +441,15 @@ export const readAppSettingsFile = async () => {
|
|||||||
}
|
}
|
||||||
const configToml = await window.electron.readFile(settingsPath)
|
const configToml = await window.electron.readFile(settingsPath)
|
||||||
const configObj = parseAppSettings(configToml)
|
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
|
return configObj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/main.ts
20
src/main.ts
@ -2,7 +2,15 @@
|
|||||||
// template that ElectronJS provides.
|
// template that ElectronJS provides.
|
||||||
|
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
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 path from 'path'
|
||||||
import url from 'url'
|
import url from 'url'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
@ -41,7 +49,7 @@ const createWindow = () => {
|
|||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
// mainWindow.webContents.openDevTools()
|
// mainWindow.webContents.openDevTools()
|
||||||
|
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// 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 maybeAbsolutePath = path.join(__dirname, filePath)
|
||||||
const bypassCustomProtocolHandlers = true
|
const bypassCustomProtocolHandlers = true
|
||||||
if (fss.existsSync(maybeAbsolutePath)) {
|
if (fss.existsSync(maybeAbsolutePath)) {
|
||||||
console.log(`Intercepted local-asbolute path ${filePath}, rebuilt it as ${maybeAbsolutePath}`)
|
console.log(
|
||||||
return net.fetch(url.pathToFileURL(maybeAbsolutePath).toString(), { bypassCustomProtocolHandlers })
|
`Intercepted local-asbolute path ${filePath}, rebuilt it as ${maybeAbsolutePath}`
|
||||||
|
)
|
||||||
|
return net.fetch(url.pathToFileURL(maybeAbsolutePath).toString(), {
|
||||||
|
bypassCustomProtocolHandlers,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
console.log(`Default fetch to ${filePath}`)
|
console.log(`Default fetch to ${filePath}`)
|
||||||
return net.fetch(request.url, { bypassCustomProtocolHandlers })
|
return net.fetch(request.url, { bypassCustomProtocolHandlers })
|
||||||
|
|||||||
Reference in New Issue
Block a user