diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml
index 80841b7b83..ca69edd3c7 100644
--- a/.github/actions/test/action.yml
+++ b/.github/actions/test/action.yml
@@ -46,9 +46,56 @@ 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)
+ echo "TARGET_DRIVE=${virtual_path}" >> $GITHUB_ENV
+ echo "ETCHER_INCLUDE_VIRTUAL_DRIVES=1" >> $GITHUB_ENV
+
+ - name: Setup Virtual Drive on Linux
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ dd if=/dev/zero of=virtual_test_disk.img bs=1M count=4096
+ virtual_path=$(sudo losetup -f --show virtual_test_disk.img)
+ echo "TARGET_DRIVE=${virtual_path}" >> $GITHUB_ENV
+
+ - name: Setup Virtual Drive on Windows
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ fsutil file createnew virtual_test_disk.img 4294967296
+
+ # Use DiskPart to attach and list volumes
+ $diskpartScript = @"
+ SELECT VDISK FILE=virtual_test_disk.img
+ ATTACH VDISK
+ LIST VOLUME
+ "@
+
+ diskpart /s $diskpartScript | Out-File -FilePath diskpart_output.txt -Encoding UTF8
+
+ # Extract the volume information from the DiskPart output
+ $diskpartOutput = Get-Content -Path diskpart_output.txt
+ $driveLine = $diskpartOutput | Select-String -Pattern "virtual_test_disk.img"
+
+ # Extract drive letter
+ $driveLetter = $null
+ if ($driveLine) {
+ $tokens = $driveLine -split "\s+"
+ $driveLetter = $tokens[2]
+ }
+
+ echo "TARGET_DRIVE=!drive_letter!:\\" >> %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,8 +104,17 @@ 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; note that they required `package` to run before
+ npm run wdio
+ # e2e suite requires administrative privileges
+ if [[ '${{ runner.os }}' == 'Windows' ]]; then
+ npm run wdio-e2e
+ else
+ sudo TARGET_DRIVE=${{ env.TARGET_DRIVE }} ETCHER_INCLUDE_VIRTUAL_DRIVES=1 npm run wdio-e2e
+ fi
+
env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: 'true'
diff --git a/.gitignore b/.gitignore
index f523e5fe17..eb19b1e4b9 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/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx
index d3c2d38955..864ab91ff3 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-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx
index 716c7acc38..b2f1469e06 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 0986bee642..7fb1cc95de 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 9192e3df75..c41eb7cd9a 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 = ({
) =>
@@ -655,6 +657,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 +668,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 b2d62869c7..23d7ac4442 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/util/drive-scanner.ts b/lib/util/drive-scanner.ts
index 30917a909c..8219f2f8dc 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 === '1',
}),
];
diff --git a/package.json b/package.json
index 1c30bdefe1..6912521071 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,9 @@
"package": "electron-forge package",
"start": "electron-forge start",
"make": "electron-forge make",
- "wdio": "xvfb-maybe wdio run ./wdio.conf.ts"
+ "wdio": "xvfb-maybe wdio run ./wdio.conf.ts --suite gui --suite shared",
+ "wdio:e2e": "xvfb-maybe wdio run ./wdio.conf.ts --suite e2e",
+ "wdio:all": "xvfb-maybe wdio run ./wdio.conf.ts --suite gui --suite shared --suite e2e"
},
"husky": {
"hooks": {
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 0000000000..fea720f966
--- /dev/null
+++ b/tests/e2e/e2e-flash-from-file.spec.ts
@@ -0,0 +1,42 @@
+import { browser } from '@wdio/globals';
+
+describe('Electron Testing', () => {
+ it('should print application title', async () => {
+ console.log('Hello', await browser.getTitle(), 'application!');
+ });
+
+ it('should "flash from file"', async () => {
+ const flashFromFileButton = $('button[data-testid="flash-from-file"]');
+ await flashFromFileButton.waitForDisplayed({ timeout: 10000 });
+ // const isDisplayed = await flashFromFileButton.isDisplayed();
+ await flashFromFileButton.click();
+
+ const selectTargetButton = $('button[data-testid="select-target-button"]');
+ await selectTargetButton.waitForClickable({ timeout: 30000 });
+ await selectTargetButton.click();
+
+ // TODO: Select target using ENV variable for the drive
+ const targetVirtualDrive = $('=/dev/disk8');
+ await targetVirtualDrive.waitForDisplayed({ timeout: 10000 });
+ await targetVirtualDrive.click();
+
+ const validateTargetButton = $(
+ 'button[data-testid="validate-target-button"]',
+ );
+ await validateTargetButton.waitForClickable({ timeout: 10000 });
+ await validateTargetButton.click();
+
+ const flashNowButton = $('button[data-testid="flash-now-button"]');
+ await flashNowButton.waitForClickable({ timeout: 10000 });
+ await flashNowButton.click();
+
+ // FIXME: not able to find the flashResults :(
+ const flashResults = $('span[data-testid="flash-results"]');
+ await flashResults.waitForDisplayed({ timeout: 20000 });
+
+ expect(flashResults.getText()).toBe('Flash Completed!');
+
+ // we're good;
+ // now we should check the content of the image but we can do that outside wdio
+ });
+});
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 0000000000..3663090833
--- /dev/null
+++ b/tests/e2e/e2e-flash-from-url.spec.ts
@@ -0,0 +1,61 @@
+import { browser } from '@wdio/globals';
+
+describe('Electron Testing', () => {
+ it('should print application title', async () => {
+ console.log('Hello', await browser.getTitle(), 'application!');
+ });
+
+ it('should "select an url source"', async () => {
+ const flashFromUrlButton = $('button[data-testid="flash-from-url"]');
+ await flashFromUrlButton.waitForDisplayed({ timeout: 10000 });
+ // const isDisplayed = await flashFromFileButton.isDisplayed();
+ await flashFromUrlButton.click();
+
+ const enterValidUrlInput = $('input[data-testid="source-url-input"]');
+ await enterValidUrlInput.waitForDisplayed({ timeout: 10000 });
+
+ // TODO: use an env variable for the URL
+ await enterValidUrlInput.setValue(
+ 'https://api.balena-cloud.com/download?deviceType=raspberrypi4-64&version=5.2.8&fileType=.zip&developmentMode=true',
+ );
+
+ const sourceUrlOkButton = $('button[data-testid="source-url-ok-button"]');
+ await sourceUrlOkButton.waitForDisplayed({ timeout: 10000 });
+ await sourceUrlOkButton.click();
+ });
+
+ 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.waitForDisplayed({ timeout: 10000 });
+ await targetVirtualDrive.click();
+
+ const validateTargetButton = $(
+ 'button[data-testid="validate-target-button"]',
+ );
+ await validateTargetButton.waitForClickable({ timeout: 10000 });
+ await validateTargetButton.click();
+ });
+
+ it('should "start flashing"', async () => {
+ const flashNowButton = $('button[data-testid="flash-now-button"]');
+ await flashNowButton.waitForClickable({ timeout: 10000 });
+ await flashNowButton.click();
+ });
+
+ 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!');
+
+ // we're good;
+ // now we should check the content of the image but we can do that outside wdio
+ });
+});
diff --git a/tests/test.e2e.ts b/tests/test.e2e.ts
deleted file mode 100644
index f3a84194d4..0000000000
--- 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 4c1e8e47d8..8100a0c3c4 100644
--- a/wdio.conf.ts
+++ b/wdio.conf.ts
@@ -35,16 +35,25 @@ 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',
+ ],
+ },
//
// ============
// Capabilities
@@ -85,7 +94,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: