diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 80841b7b83b..b5bafc48d5d 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -46,9 +46,20 @@ runs: with: python-version: '3.11' + - name: Setup Virtual Drive on MacOS + if: runner.os == 'macOS' + shell: bash + run: | + hdiutil create -size 4096m -layout NONE -o virtual_test_disk.dmg + virtual_path=$(hdiutil attach -nomount virtual_test_disk.dmg | awk '{print $1}') + echo "TARGET_DRIVE=${virtual_path}" >> $GITHUB_ENV + echo "ETCHER_INCLUDE_VIRTUAL_DRIVES=1" >> $GITHUB_ENV + - name: Test release shell: bash run: | + # Build and Test release + ## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled # if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then # export DEBUG='electron-forge:*,sidecar' @@ -57,11 +68,29 @@ runs: npm ci npm run lint npm run package - npm run wdio # test stage, note that it requires the package to be done first + + # tests requires the app to already be built + # # only run e2e tests on Mac as it's the only supported platform atm + if [[ '${{ runner.os }}' == 'macOS' ]]; then + # run all tests on macOS including E2E + # E2E tests can't input the administrative password, therefore the tests need to run as root + wget -q -O ${{ env.TEST_SOURCE_FILE }} ${{ env.TEST_SOURCE_URL }} + sudo \ + TARGET_DRIVE=${{ env.TARGET_DRIVE }} \ + ETCHER_INCLUDE_VIRTUAL_DRIVES=1 \ + TEST_SOURCE_FILE: $(pwd)/${{ env.TEST_SOURCE_FILE }} \ + TEST_SOURCE_URL: ${{ env.TEST_SOURCE_URL }} \ + npm run wdio:ci + else + npm run wdio:unit + fi + env: # https://www.electronjs.org/docs/latest/api/environment-variables ELECTRON_NO_ATTACH_CONSOLE: 'true' + TEST_SOURCE_URL: 'https://api.balena-cloud.com/download?deviceType=raspberrypi4-64&version=5.2.8&fileType=.zip' + TEST_SOURCE_FILE: 'raspberrypi4-64-5.2.8-v16.1.10.img.zip' - name: Compress custom source if: runner.os != 'Windows' diff --git a/.gitignore b/.gitignore index f523e5fe176..eb19b1e4b95 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,10 @@ secrets/WINDOWS_SIGNING.pfx #local development .yalc -yalc.lock \ No newline at end of file +yalc.lock + +# Test assets +virtual_test_disk.dmg +virtual_test_disk.img +virtual_test_disk.vhd +screenshots/ \ No newline at end of file diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index 5457909c984..59a9ce0f0f7 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -130,14 +130,18 @@ observe(() => { function setDrives(drives: Dictionary) { // prevent setting drives while flashing otherwise we might lose some while we unmount them - if (!flashState.isFlashing()) { - availableDrives.setDrives(values(drives)); - } + availableDrives.setDrives(values(drives)); } // Spawning the child process without privileges to get the drives list // TODO: clean up this mess of exports -export let requestMetadata: any; +export let requestMetadata: (params: any) => Promise; +export let startScanner: () => void = () => { + console.log('stopScanner is not yet set'); +}; +export let stopScanner: () => void = () => { + console.log('stopScanner is not yet set'); +}; // start the api and spawn the child process spawnChildAndConnect({ @@ -147,6 +151,18 @@ spawnChildAndConnect({ // start scanning emit('scan', {}); + // make startScanner available for the end of flash + startScanner = () => { + console.log('startScanner'); + emit('scan', {}); + }; + + // make stopScanner available for the start of flash + stopScanner = () => { + console.log('stopScanner'); + emit('scan', {}); + }; + // make the sourceMetada awaitable to be used on source selection requestMetadata = async (params: any): Promise => { emit('sourceMetadata', JSON.stringify(params)); diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx index d3c2d38955f..864ab91ff3d 100644 --- a/lib/gui/app/components/drive-selector/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector/drive-selector.tsx @@ -419,6 +419,7 @@ export class DriveSelector extends React.Component< primary: !showWarnings, warning: showWarnings, disabled: !hasAvailableDrives(), + 'data-testid': 'validate-target-button', }} {...props} > diff --git a/lib/gui/app/components/flash-another/flash-another.tsx b/lib/gui/app/components/flash-another/flash-another.tsx index c2246317a79..85a732e7da7 100644 --- a/lib/gui/app/components/flash-another/flash-another.tsx +++ b/lib/gui/app/components/flash-another/flash-another.tsx @@ -25,7 +25,11 @@ export interface FlashAnotherProps { export const FlashAnother = (props: FlashAnotherProps) => { return ( - + {i18next.t('flash.another')} ); diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx index 716c7acc38a..b2f1469e06b 100644 --- a/lib/gui/app/components/flash-results/flash-results.tsx +++ b/lib/gui/app/components/flash-results/flash-results.tsx @@ -163,7 +163,7 @@ export function FlashResults({ /> {middleEllipsis(image, 24)} - + {allFailed ? i18next.t('flash.flashFailed') : i18next.t('flash.flashCompleted')} diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx index 0986bee6429..7fb1cc95de5 100644 --- a/lib/gui/app/components/progress-button/progress-button.tsx +++ b/lib/gui/app/components/progress-button/progress-button.tsx @@ -104,7 +104,9 @@ export class ProgressButton extends React.PureComponent { }} > - {status}  + + {status}  + {position} {type && ( @@ -125,6 +127,7 @@ export class ProgressButton extends React.PureComponent { warning={warning} onClick={this.props.callback} disabled={this.props.disabled} + data-testid={'flash-now-button'} style={{ marginTop: 30, }} diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 9192e3df759..06686808b58 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -165,6 +165,7 @@ const URLSelector = ({ cancel={cancel} primaryButtonProps={{ disabled: loading || !imageURL, + 'data-testid': 'source-url-ok-button', }} action={loading ? : i18next.t('ok')} done={async () => { @@ -186,6 +187,7 @@ const URLSelector = ({ ) => @@ -638,6 +640,7 @@ export class SourceSelector extends React.Component< {!flashing && !imageLoading && ( this.reselectSource()} @@ -655,6 +658,7 @@ export class SourceSelector extends React.Component< disabled={this.state.imageSelectorOpen} primary={this.state.defaultFlowActive} key="Flash from file" + data-testid="flash-from-file" flow={{ onClick: () => this.openImageSelector(), label: i18next.t('source.fromFile'), @@ -665,6 +669,7 @@ export class SourceSelector extends React.Component< /> this.openURLSelector(), label: i18next.t('source.fromURL'), diff --git a/lib/gui/app/components/target-selector/target-selector-button.tsx b/lib/gui/app/components/target-selector/target-selector-button.tsx index b2d62869c7c..23d7ac4442d 100644 --- a/lib/gui/app/components/target-selector/target-selector-button.tsx +++ b/lib/gui/app/components/target-selector/target-selector-button.tsx @@ -150,6 +150,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) { tabIndex={targets.length > 0 ? -1 : 2} disabled={props.disabled} onClick={props.openDriveSelector} + data-testid="select-target-button" > {i18next.t('target.selectTarget')} diff --git a/lib/gui/app/modules/progress-status.ts b/lib/gui/app/modules/progress-status.ts index 02ec22301c0..b04956347b6 100644 --- a/lib/gui/app/modules/progress-status.ts +++ b/lib/gui/app/modules/progress-status.ts @@ -34,8 +34,6 @@ export function fromFlashState({ status: string; position?: string; } { - console.log(i18next.t('progress.starting')); - if (type === undefined) { return { status: i18next.t('progress.starting') }; } else if (type === 'decompressing') { diff --git a/lib/gui/app/os/dialog.ts b/lib/gui/app/os/dialog.ts index b882dced686..a1ac1ede4f1 100644 --- a/lib/gui/app/os/dialog.ts +++ b/lib/gui/app/os/dialog.ts @@ -23,6 +23,7 @@ import * as settings from '../../../gui/app/models/settings'; import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats'; import * as i18next from 'i18next'; +// FIXME: this is probably useless now async function mountSourceDrive() { // sourceDrivePath is the name of the link in /dev/disk/by-path const sourceDrivePath = await settings.get('automountOnFileSelect'); @@ -43,6 +44,17 @@ async function mountSourceDrive() { */ export async function selectImage(): Promise { await mountSourceDrive(); + + // For automated E2E testing, we can't set the source file by interacting with the OS dialog, + // so we use an ENV var instead and bypass the dialog. Note that we still need to press the "flash from file" button. + if ( + process.env.TEST_SOURCE_FILE !== undefined && + typeof process.env.TEST_SOURCE_FILE === 'string' + ) { + console.log(`test mode: loading ${process.env.TEST_SOURCE_FILE}`); + return process.env.TEST_SOURCE_FILE; + } + const options: electron.OpenDialogOptions = { // This variable is set when running in GNU/Linux from // inside an AppImage, and represents the working directory diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 25aa42603ba..0e87359435d 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -122,8 +122,6 @@ async function flashImageToDrive( errorMessage = messages.error.genericFlashError(error); } return errorMessage; - } finally { - availableDrives.setDrives([]); } return ''; diff --git a/lib/util/api.ts b/lib/util/api.ts index 0fe1e40246e..f2ce0b88e9a 100644 --- a/lib/util/api.ts +++ b/lib/util/api.ts @@ -24,7 +24,7 @@ import { toJSON } from '../shared/errors'; import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes'; import type { WriteOptions } from './types/types'; import { write, cleanup } from './child-writer'; -import { startScanning } from './scanner'; +import { startScanning, stopScanning } from './scanner'; import { getSourceMetadata } from './source-metadata'; import type { DrivelistDrive } from '../shared/drive-constraints'; import type { SourceMetadata } from '../shared/typings/source-selector'; @@ -222,6 +222,11 @@ function setup(): Promise { startScanning(); }, + stopScan: () => { + log('Stop scan requested'); + stopScanning(); + }, + // route `cancel` from client cancel: () => onAbort(GENERAL_ERROR), diff --git a/lib/util/drive-scanner.ts b/lib/util/drive-scanner.ts index 30917a909c8..d92049f3052 100644 --- a/lib/util/drive-scanner.ts +++ b/lib/util/drive-scanner.ts @@ -25,6 +25,8 @@ import { geteuid, platform } from 'process'; const adapters: Adapter[] = [ new BlockDeviceAdapter({ includeSystemDrives: () => true, + includeVirtualDrives: () => + process.env.ETCHER_INCLUDE_VIRTUAL_DRIVES !== 'undefined', }), ]; diff --git a/lib/util/scanner.ts b/lib/util/scanner.ts index 323d1ea938d..5f3a57a2d4f 100644 --- a/lib/util/scanner.ts +++ b/lib/util/scanner.ts @@ -172,12 +172,15 @@ const COMPUTE_MODULE_DESCRIPTIONS: Dictionary = { }; const startScanning = () => { - driveScanner.on('attach', (drive) => addDrive(drive)); - driveScanner.on('detach', (drive) => removeDrive(drive)); + driveScanner.on('attach', addDrive); + driveScanner.on('detach', removeDrive); driveScanner.start(); }; const stopScanning = () => { + driveScanner.removeListener('attach', addDrive); + driveScanner.removeListener('detach', removeDrive); + availableDrives = []; driveScanner.stop(); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 64171f1377e..e9415a2ba0f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -76,11 +76,11 @@ "string-replace-loader": "3.1.0", "style-loader": "3.3.3", "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", "tslib": "2.6.2", "typescript": "^5.3.3", "url-loader": "4.1.1", - "wdio-electron-service": "^6.4.1", + "wdio-electron-service": "^6.5.0", "xvfb-maybe": "^0.2.1" }, "engines": { @@ -5798,6 +5798,18 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, "node_modules/@types/styled-components": { "version": "5.1.34", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", @@ -6133,9 +6145,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz", - "integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -9485,6 +9497,18 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -11435,6 +11459,15 @@ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -13672,15 +13705,16 @@ } }, "node_modules/find-versions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", - "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", + "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", "dev": true, "dependencies": { - "semver-regex": "^4.0.5" + "semver-regex": "^4.0.5", + "super-regex": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -14071,6 +14105,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.1.tgz", + "integrity": "sha512-6yPMImFFuaMPNaTMTBuolA8EanHJWF5Vju0NHpObRURT105J6x1Mf2a7J4P7Sqk2xDxv24N5L0RatEhTBhNmdA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/function.prototype.name": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", @@ -22630,9 +22676,9 @@ } }, "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.17.0.tgz", - "integrity": "sha512-9flrz1zkfLRH3jO3bLflmTxryzKMxVa7841VeMgBaNQGY6vH4RCcpN/sQLB7mQQYh1GZ5utT2deypMuCy4yicw==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", + "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", "dev": true, "engines": { "node": ">=16" @@ -25116,6 +25162,22 @@ "node": ">= 8.0" } }, + "node_modules/super-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.0.0.tgz", + "integrity": "sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==", + "dev": true, + "dependencies": { + "function-timeout": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", @@ -25470,6 +25532,21 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -25636,6 +25713,15 @@ "node": "*" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -25765,6 +25851,72 @@ } } }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/ts-node/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -25774,6 +25926,36 @@ "node": ">=0.3.1" } }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -26841,9 +27023,9 @@ } }, "node_modules/wdio-electron-service": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/wdio-electron-service/-/wdio-electron-service-6.4.1.tgz", - "integrity": "sha512-mYF3ZXuQc9BJlWK89taFMJy5bTKEjjU/ISR//D48lMnSksUhIy7NMPhGf6rob7EaU5XCzgLvqXbpX2ZviNk2YQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/wdio-electron-service/-/wdio-electron-service-6.5.0.tgz", + "integrity": "sha512-8W+3ZoAvQOgqr6rOsKtL5h4dq8Cyj5+J24kzwOUykjZVFSljj7suvpvfFqYaPEQMGXLVMXlhwX/RCWEJt0c2Rg==", "dev": true, "dependencies": { "@vitest/spy": "^1.2.0", @@ -26852,7 +27034,7 @@ "debug": "^4.3.4", "electron-to-chromium": "^1.4.630", "fast-copy": "^3.0.1", - "find-versions": "^5.1.0", + "find-versions": "^6.0.0", "node-fetch": "^3.3.2", "read-package-up": "^11.0.0" }, diff --git a/package.json b/package.json index 1c30bdefe1b..f095fe10900 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,11 @@ "test": "echo 'Only use custom tests; if you want to test locally, use `npm run wdio`' && exit 0", "package": "electron-forge package", "start": "electron-forge start", + "dev:util": "ETCHER_TERMINATE_TIMEOUT=0 ts-node-dev --respawn --transpile-only --no-notify --no-deps --prefer-ts-exts --files -- ./lib/util/api.ts", "make": "electron-forge make", + "wdio:unit": "xvfb-maybe wdio run ./wdio.conf.ts --suite gui --suite shared", + "wdio:e2e": "xvfb-maybe wdio run ./wdio.conf.ts --suite e2e", + "wdio:ci": "xvfb-maybe wdio run ./wdio.conf.ts --suite ci", "wdio": "xvfb-maybe wdio run ./wdio.conf.ts" }, "husky": { @@ -97,11 +101,11 @@ "string-replace-loader": "3.1.0", "style-loader": "3.3.3", "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", "tslib": "2.6.2", "typescript": "^5.3.3", "url-loader": "4.1.1", - "wdio-electron-service": "^6.4.1", + "wdio-electron-service": "^6.5.0", "xvfb-maybe": "^0.2.1" }, "hostDependencies": { diff --git a/tests/e2e/e2e-common.ts b/tests/e2e/e2e-common.ts new file mode 100644 index 00000000000..0d71d0a6dca --- /dev/null +++ b/tests/e2e/e2e-common.ts @@ -0,0 +1,71 @@ +import { browser } from '@wdio/globals'; + +const prepare = () => { + browser.pause(2000); +}; + +const itShouldSelectVirtualTarget = () => { + it('should "select a virtual target"', async () => { + const selectTargetButton = $('button[data-testid="select-target-button"]'); + await selectTargetButton.waitForClickable({ timeout: 30000 }); + await selectTargetButton.click(); + + // target drive is set in the github custom test action + // if you run the test locally, pass the varibale + const targetVirtualDrive = $(`=${process.env.TARGET_DRIVE}`); + await targetVirtualDrive.waitForClickable({ timeout: 10000 }); + await targetVirtualDrive.click(); + + const validateTargetButton = $( + 'button[data-testid="validate-target-button"]', + ); + await validateTargetButton.waitForClickable({ timeout: 10000 }); + await validateTargetButton.click(); + }); +}; + +const itShouldStartFlashing = () => { + it('should "start flashing"', async () => { + const flashNowButton = $('button[data-testid="flash-now-button"]'); + await flashNowButton.waitForClickable({ timeout: 10000 }); + await flashNowButton.click(); + }); +}; + +const itShouldGetTheFlashCompletedScreen = () => { + it('should get the "Flash Completed" screen', async () => { + const flashResults = $('[data-testid="flash-results"]'); + await flashResults.waitForDisplayed({ timeout: 180000 }); + + const flashResultsText = await flashResults.getText(); + expect(flashResultsText).toBe('Flash Completed!'); + }); +}; + +const itShouldGetBackToHomeScreen = () => { + it('should get back to the "Home" screen', async () => { + const flashAnotherButton = $('[data-testid="flash-another-button"]'); + + await flashAnotherButton.waitForClickable({ timeout: 10000 }); + await flashAnotherButton.click(); + + const changeSource = $('[data-testid="change-image-button"]'); + + await changeSource.waitForClickable({ timeout: 10000 }); + await changeSource.click(); + + // const flashResultsText = await flashResults.getText(); + // expect(flashResultsText).toBe('Flash Completed!'); + + // we're good; + // now we should check the content of the image but we can do that outside wdio + }); +}; + +export { + prepare, + itShouldSelectVirtualTarget, + itShouldStartFlashing, + itShouldGetTheFlashCompletedScreen, + itShouldGetBackToHomeScreen, +}; diff --git a/tests/e2e/e2e-flash-from-file.spec.ts b/tests/e2e/e2e-flash-from-file.spec.ts new file mode 100644 index 00000000000..0fc028b1216 --- /dev/null +++ b/tests/e2e/e2e-flash-from-file.spec.ts @@ -0,0 +1,23 @@ +import '@wdio/globals'; +import { + prepare, + itShouldSelectVirtualTarget, + itShouldStartFlashing, + itShouldGetTheFlashCompletedScreen, + itShouldGetBackToHomeScreen, +} from './e2e-common'; + +describe('Flash From File E2E Test', () => { + before(prepare); + + it('should select a file as source', async () => { + const flashFromFileButton = $('button[data-testid="flash-from-file"]'); + await flashFromFileButton.waitForClickable({ timeout: 10000 }); + await flashFromFileButton.click(); + }); + + itShouldSelectVirtualTarget(); + itShouldStartFlashing(); + itShouldGetTheFlashCompletedScreen(); + itShouldGetBackToHomeScreen(); +}); diff --git a/tests/e2e/e2e-flash-from-url.spec.ts b/tests/e2e/e2e-flash-from-url.spec.ts new file mode 100644 index 00000000000..7f87c42207a --- /dev/null +++ b/tests/e2e/e2e-flash-from-url.spec.ts @@ -0,0 +1,31 @@ +import '@wdio/globals'; +import { + prepare, + itShouldSelectVirtualTarget, + itShouldStartFlashing, + itShouldGetTheFlashCompletedScreen, + itShouldGetBackToHomeScreen, +} from './e2e-common'; + +describe('Flash From URL E2E test', () => { + before(prepare); + + it('should select an url as source', async () => { + const flashFromUrlButton = $('button[data-testid="flash-from-url"]'); + await flashFromUrlButton.waitForClickable({ timeout: 10000 }); + await flashFromUrlButton.click(); + + const enterValidUrlInput = $('input[data-testid="source-url-input"]'); + await enterValidUrlInput.waitForDisplayed({ timeout: 10000 }); + await enterValidUrlInput.setValue(process.env.TEST_SOURCE_URL as string); + + const sourceUrlOkButton = $('button[data-testid="source-url-ok-button"]'); + await sourceUrlOkButton.waitForClickable({ timeout: 10000 }); + await sourceUrlOkButton.click(); + }); + + itShouldSelectVirtualTarget(); + itShouldStartFlashing(); + itShouldGetTheFlashCompletedScreen(); + itShouldGetBackToHomeScreen(); +}); diff --git a/tests/test.e2e.ts b/tests/test.e2e.ts deleted file mode 100644 index f3a84194d46..00000000000 --- a/tests/test.e2e.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { browser } from '@wdio/globals'; - -describe('Electron Testing', () => { - it('should print application title', async () => { - console.log('Hello', await browser.getTitle(), 'application!'); - }); -}); diff --git a/wdio.conf.ts b/wdio.conf.ts index 4c1e8e47d88..b5e1cc7345b 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -35,16 +35,36 @@ export const config: Options.Testrunner = { // Patterns to exclude. // FIXME: Remove the following exclusions once the tests are ported to WDIO exclude: [ - 'tests/gui/modules/image-writer.spec.ts', - 'tests/gui/os/window-progress.spec.ts', - 'tests/gui/models/available-drives.spec.ts', - 'tests/gui/models/flash-state.spec.ts', - 'tests/gui/models/selection-state.spec.ts', - 'tests/gui/models/settings.spec.ts', - 'tests/shared/drive-constraints.spec.ts', - 'tests/shared/messages.spec.ts', - 'tests/gui/modules/progress-status.spec.ts', + './tests/gui/modules/image-writer.spec.ts', + './tests/gui/os/window-progress.spec.ts', + './tests/gui/models/available-drives.spec.ts', + './tests/gui/models/flash-state.spec.ts', + './tests/gui/models/selection-state.spec.ts', + './tests/gui/models/settings.spec.ts', + './tests/shared/drive-constraints.spec.ts', + './tests/shared/messages.spec.ts', + './tests/gui/modules/progress-status.spec.ts', ], + + suites: { + 'gui': ['./tests/gui/**/*.spec.ts'], + 'shared': ['./tests/shared/**/*.spec.ts'], + 'e2e': [ + [ + './tests/e2e/e2e-flash-from-file.spec.ts', + './tests/e2e/e2e-flash-from-url.spec.ts', + ] + ], + // CI needs to runs e2e tests and other tests sequencially + 'ci': [ + [ + './tests/e2e/e2e-flash-from-url.spec.ts', + './tests/e2e/e2e-flash-from-file.spec.ts', + './tests/gui/**/*.spec.ts', + './tests/shared/**/*.spec.ts', + ], + ] + }, // // ============ // Capabilities @@ -85,7 +105,7 @@ export const config: Options.Testrunner = { // Define all options that are relevant for the WebdriverIO instance here // // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: 'info', + logLevel: 'warn', // // Set specific log levels per logger // loggers: