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 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))
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -66,3 +66,6 @@ venv
|
||||
|
||||
# electron
|
||||
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 {
|
||||
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()
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
18
src/main.ts
18
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'
|
||||
@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user