Skip to content

Commit

Permalink
patch: basic E2E tests for macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
aethernet committed May 7, 2024
1 parent 6a648e9 commit 2d3f2c8
Show file tree
Hide file tree
Showing 22 changed files with 459 additions and 52 deletions.
31 changes: 30 additions & 1 deletion .github/actions/test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,10 @@ secrets/WINDOWS_SIGNING.pfx

#local development
.yalc
yalc.lock
yalc.lock

# Test assets
virtual_test_disk.dmg
virtual_test_disk.img
virtual_test_disk.vhd
screenshots/
24 changes: 20 additions & 4 deletions lib/gui/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,18 @@ observe(() => {

function setDrives(drives: Dictionary<DrivelistDrive>) {
// 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<SourceMetadata>;
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({
Expand All @@ -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<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));
Expand Down
1 change: 1 addition & 0 deletions lib/gui/app/components/drive-selector/drive-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export class DriveSelector extends React.Component<
primary: !showWarnings,
warning: showWarnings,
disabled: !hasAvailableDrives(),
'data-testid': 'validate-target-button',
}}
{...props}
>
Expand Down
6 changes: 5 additions & 1 deletion lib/gui/app/components/flash-another/flash-another.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export interface FlashAnotherProps {

export const FlashAnother = (props: FlashAnotherProps) => {
return (
<BaseButton primary onClick={props.onClick}>
<BaseButton
primary
data-testid="flash-another-button"
onClick={props.onClick}
>
{i18next.t('flash.another')}
</BaseButton>
);
Expand Down
2 changes: 1 addition & 1 deletion lib/gui/app/components/flash-results/flash-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function FlashResults({
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
<Txt data-testid="flash-results" fontSize={24} color="#fff" mb="17px">
{allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
Expand Down
5 changes: 4 additions & 1 deletion lib/gui/app/components/progress-button/progress-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt data-testid="flash-status" color="#fff">
{status}&nbsp;
</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
{type && (
Expand All @@ -125,6 +127,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
warning={warning}
onClick={this.props.callback}
disabled={this.props.disabled}
data-testid={'flash-now-button'}
style={{
marginTop: 30,
}}
Expand Down
5 changes: 5 additions & 0 deletions lib/gui/app/components/source-selector/source-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const URLSelector = ({
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
'data-testid': 'source-url-ok-button',
}}
action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => {
Expand All @@ -186,6 +187,7 @@ const URLSelector = ({
</Txt>
<Input
value={imageURL}
data-testid="source-url-input"
placeholder={i18next.t('source.enterValidURL')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
Expand Down Expand Up @@ -638,6 +640,7 @@ export class SourceSelector extends React.Component<
</StepNameButton>
{!flashing && !imageLoading && (
<ChangeButton
data-testid="change-image-button"
plain
mb={14}
onClick={() => this.reselectSource()}
Expand All @@ -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'),
Expand All @@ -665,6 +669,7 @@ export class SourceSelector extends React.Component<
/>
<FlowSelector
key="Flash from URL"
data-testid="flash-from-url"
flow={{
onClick: () => this.openURLSelector(),
label: i18next.t('source.fromURL'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
</StepButton>
Expand Down
2 changes: 0 additions & 2 deletions lib/gui/app/modules/progress-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
12 changes: 12 additions & 0 deletions lib/gui/app/os/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -43,6 +44,17 @@ async function mountSourceDrive() {
*/
export async function selectImage(): Promise<string | undefined> {
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
Expand Down
2 changes: 0 additions & 2 deletions lib/gui/app/pages/main/Flash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ async function flashImageToDrive(
errorMessage = messages.error.genericFlashError(error);
}
return errorMessage;
} finally {
availableDrives.setDrives([]);
}

return '';
Expand Down
7 changes: 6 additions & 1 deletion lib/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -222,6 +222,11 @@ function setup(): Promise<EmitLog> {
startScanning();
},

stopScan: () => {
log('Stop scan requested');
stopScanning();
},

// route `cancel` from client
cancel: () => onAbort(GENERAL_ERROR),

Expand Down
2 changes: 2 additions & 0 deletions lib/util/drive-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { geteuid, platform } from 'process';
const adapters: Adapter[] = [
new BlockDeviceAdapter({
includeSystemDrives: () => true,
includeVirtualDrives: () =>
process.env.ETCHER_INCLUDE_VIRTUAL_DRIVES !== 'undefined',
}),
];

Expand Down
7 changes: 5 additions & 2 deletions lib/util/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,15 @@ const COMPUTE_MODULE_DESCRIPTIONS: Dictionary<string> = {
};

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();
};

Expand Down
Loading

0 comments on commit 2d3f2c8

Please sign in to comment.