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: