Aller au contenu principal

Tests automatisés

L'automatisation des tests est un moyen efficace de valider que le code de votre application fonctionne comme prévu. Bien qu'Electron ne maintienne pas activement sa propre solution de test, ce guide va présenter deux façons de faire des tests automatisés de bout en bout sur votre application Electron.

En utilisant l’interface WebDriver

Extrait de: ChromeDriver - WebDriver pour Chrome :

WebDriver est un outil open source pour les tests automatisés d'applications web sur plusieurs navigateurs. Il fournit des fonctions pour naviguer vers les pages web, simuler l'input utilisateur, l’exécution de JavaScript, etc... . ChromeDriver est un serveur autonome qui implémente le protocole de WebDriver (WebDriver's wire protocol) pour Chromium. Il est développé par des membres des équipes chrome et WebDriver.

Il y a plusieurs façons de configurer les tests en utilisant WebDriver.

Avec WebdriverIO

WebdriverIO (WDIO) est un framework d'automatisation de test fournissant un package Node.js pour tester avec WebDriver. Son écosystème comprend également divers plugins (par exemple, reporter et services) qui peuvent vous aider à assembler votre configuration de test.

Si vous avez déjà une configuration WebdriverIO existante, il est recommandé de mettre à jour vos dépendances et de valider votre configuration existante en suivant la description dans la documentation.

Installation du lanceur de tests

Si vous n'utilisez pas encore WebdriverIO dans votre projet, vous pouvez l'ajouter en exécutant le toolkit de démarrage dans le répertoire racine de votre projet :

npm init wdio@latest ./

This starts a configuration wizard that helps you put together the right setup, installs all necessary packages, and generates a wdio.conf.js configuration file. Make sure to select "Desktop Testing - of Electron Applications" on one of the first questions asking "What type of testing would you like to do?".

Connexion de WDIO à votre application Electron

After running the configuration wizard, your wdio.conf.js should include roughly the following content:

wdio.conf.js
export const config = {
// ...
services: ['electron'],
capabilities: [{
browserName: 'electron',
'wdio:electronServiceOptions': {
// WebdriverIO can automatically find your bundled application
// if you use Electron Forge or electron-builder, otherwise you
// can define it here, e.g.:
// appBinaryPath: './path/to/bundled/application.exe',
appArgs: ['foo', 'bar=baz']
}
}]
// ...
}

Rédaction des tests

Use the WebdriverIO API to interact with elements on the screen. The framework provides custom "matchers" that make asserting the state of your application easy, e.g.:

import { browser, $, expect } from '@wdio/globals'

describe('keyboard input', () => {
it('should detect keyboard input', async () => {
await browser.keys(['y', 'o'])
await expect($('keypress-count')).toHaveText('YO')
})
})

Furthermore, WebdriverIO allows you to access Electron APIs to get static information about your application:

import { browser, $, expect } from '@wdio/globals'

describe('when the make smaller button is clicked', () => {
it('should decrease the window height and width by 10 pixels', async () => {
const boundsBefore = await browser.electron.browserWindow('getBounds')
expect(boundsBefore.width).toEqual(210)
expect(boundsBefore.height).toEqual(310)

await $('.make-smaller').click()
const boundsAfter = await browser.electron.browserWindow('getBounds')
expect(boundsAfter.width).toEqual(200)
expect(boundsAfter.height).toEqual(300)
})
})

or to retrieve other Electron process information:

import fs from 'node:fs'
import path from 'node:path'
import { browser, expect } from '@wdio/globals'

const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
const { name, version } = packageJson

describe('electron APIs', () => {
it('should retrieve app metadata through the electron API', async () => {
const appName = await browser.electron.app('getName')
expect(appName).toEqual(name)
const appVersion = await browser.electron.app('getVersion')
expect(appVersion).toEqual(version)
})

it('should pass args through to the launched application', async () => {
// custom args are set in the wdio.conf.js file as they need to be set before WDIO starts
const argv = await browser.electron.mainProcess('argv')
expect(argv).toContain('--foo')
expect(argv).toContain('--bar=baz')
})
})

Lancement des tests

Pour exécuter vos tests:

$ npx wdio run wdio.conf.js

WebdriverIO helps launch and shut down the application for you.

Pour d'avantage de documentation

Find more documentation on Mocking Electron APIs and other useful resources in the official WebdriverIO documentation.

Avec sélénium

Selenium est un framework d’automatisation Web exposant des liaisons aux API WebDriver dans de nombreux languages. Les liaisons Node.js sont disponibles dans le package selenium-webdriver sur NPM.

Démarrage d'un serveur ChromeDriver

Afin d'utiliser Selenium avec Electron, vous devez télécharger le binaire electron-chromedriver et le lancer :

npm install --save-dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

N'oubliez pas le numéro du port 9515, qui servira plus tard.

Connexion de Selenium à ChromeDriver

Vous allez maintenant installer Selenium dans votre projet :

npm install --save-dev selenium-webdriver

L'utilisation de selenium-webdriver avec Electron est pratiquement la même qu’avec les sites Web normaux, la différence est que vous devez spécifier manuellement comment connecter ChromeDriver et où trouver le binaire de votre application Electron:

test.js
const webdriver = require('selenium-webdriver')
const driver = new webdriver.Builder()
// "9515" est le port ouvert par le ChromeDriver.
.usingServer('http://localhost:9515')
.withCapabilities({
'goog:chromeOptions': {
// Le chemin d'accès vers votre binaire Electron.
binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
}
})
.forBrowser('chrome') // note : utiliser .forBrowser('electron') pour selenium-webdriver <= 3.6.0
.build()
driver.get('https://www.google.com')
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
driver.findElement(webdriver.By.name('btnG')).click()
driver.wait(() => {
return driver.getTitle().then((title) => {
return title === 'webdriver - Google Search'
})
}, 1000)
driver.quit()

En utilisant Playwright

Microsoft Playwright est un framework de test complet utilisant les protocoles de débogage à distance spécifiques au navigateur, similaire à l’API sans affichage Puppeteer, mais orienté vers une solution globale de test. Playwright prend en charge expérimentalement Electron via la prise en charge par Electron du protocole Chrome DevTools (CDP).

Installation des dépendances

Vous pouvez installer Playwright via votre gestionnaire de packages Node.js préféré. It comes with its own test runner, which is built for end-to-end testing:

npm install --save-dev @playwright/test
Concernant les Dépendances

Ce tutoriel a été écrit avec @playwright/test@1.41.1. Consultez la page sur les versions de Playwright pour connaître les changements susceptibles d'affecter le code ci-dessous.

Rédaction des tests

Playwright lance votre application en mode développement via l'API _electron.launch. Pour faire pointer cette API vers votre application Electron, vous devez fournir en argument le chemin du point d'entrée de votre processus principal (ici, il s'agira de main.js).

const { test, _electron: electron } = require('@playwright/test')

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
// fermeture de l'app
await electronApp.close()
})

Vous obtiendrez alors une instance de la classe ElectronApp de Playwright. Il s'agit d'une classe aux fonctionnalités puissantes ayant accès aux modules du processus principal comme dans l'exemple suivant:

const { test, _electron: electron } = require('@playwright/test')

test('get isPackaged', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// Cela s'exécute dans le processus principal d'Electron, le paramètre ici présent est toujours
// le résultat du require('electron') dans le script principal de l'app.
return app.isPackaged
})
console.log(isPackaged) // false (parce que nous sommes en mode développement)
// close app
wait electronApp.close()
})

On peut également créer des objets individuels Page à partir d'instances de BrowserWindow d'Electon. Par exemple, comme ci-dessous, pour capturer la premiere BrowserWindow et enregistrer une capture d’écran :

const { test, _electron: electron } = require('@playwright/test')

test('save screenshot', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })
// fermeture de l'app
await electronApp.close()
})

En rassemblant tout cela à l’aide du test-runner de Playwright, créons le fichier de test example.spec.js ne présentant qu'un seul test et une seule assertion :

example.spec.js
const { test, expect, _electron: electron } = require('@playwright/test')

test('example test', async () => {
const electronApp = await electron.launch({ args: ['.'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// Cela s'exécute dans le processus principal d'Electron, le paramètre ici est toujours
// le résultat du require('electron') dans le script principal de l'application.
return app.isPackaged
})

expect(isPackaged).toBe(false)

// Attends que la première BrowserWindow s'ouvre
// et retourne son objet Page
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })

// ferme l'app
await electronApp.close()
})

Il faut maintenat exécuter Playwright Test à l’aide de npx playwright test. Vous devez voir la réussite du test dans votre console et avoir une capture d’écran intro.png enregistrée dans le système de fichiers.

☁ $ npx playwright test

Exécute 1 test en utilisant 1 worker

✓ example.spec.js:4:1 › exemple test (1s)
info

Playwright Test exécutera automatiquement tous les fichiers satisfaisant a la regex .*(test|spec)\.(js|ts|mjs) . Vous pouvez personnaliser cette correspondance dans les options de configuration Playwright Test. Cela fonctionne également directement avec TypeScript.

Lectures complémentaires

Consultez la documentation de Playwright pour les APIs des classes d'Electron et de ElectronApplication.

En utilisant un pilote de test personnalisé

Il est également possible d'écrire son propre driver personnalisé à l'aide de l'IPC-over-STDIO intégré à Node.js. Les pilotes de test personnalisés exigent que vous écriviez du code d'application supplémentaire, mais nécessitent moins de code et vous permettent d'exposer des méthodes personnalisées à votre suite de test.

Pour créer un pilote personnalisé, nous utiliserons l’API child_process de Node.js. La suite de tests engendrera le processus Electron, puis établira un protocole de messagerie basique :

testDriver.js
const childProcess = require('node:child_process')
const electronPath = require('electron')

// création du processus
const env = { /* ... */ }
const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })

// écoute des messages IPC venant de l'application
appProcess.on('message', (msg) => {
// ...
})

// envoi d'un message IPC à l'application
appProcess.send({ my: 'message' })

Vous pouvez écouter des messages et envoyer des réponses à l’aide de l’API Node.js process depuis l’application Electron, :

main.js
// écoute des messages provenant de la suite de test
process.on('message', (msg) => {
// ...
})

// envoi d'un message à la suite de test
process.send({ my: 'message' })

La communication vers l'application Electron à partir de la suite de tests est maintenant possible en utilisant l'objet appProcess.

Pour plus de commodité, vous pouvez encapsuler appProcess dans un objet testeur qui fournira des fonctions de plus haut niveau. Voici un exemple montrant comment vous pouvez procéder. Commençons par créer la classe TestDriver :

testDriver.js
class TestDriver {
constructor ({ path, args, env }) {
this.rpcCalls = []

// démarrage du processus enfant
env.APP_TEST_DRIVER = 1 // faisons savoir à l'app qu'elle doit être à l'écoute de messages
this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })

// gestion des réponses aux ipc
this.process.on('message', (message) => {
// pop the handler
const rpcCall = this.rpcCalls[message.msgId]
if (!rpcCall) return
this.rpcCalls[message.msgId] = null
// reject/resolve
if (message.reject) rpcCall.reject(message.reject)
else rpcCall.resolve(message.resolve)
})

// attente du ready
this.isReady = this.rpc('isReady').catch((err) => {
console.error('Application failed to start', err)
this.stop()
process.exit(1)
})
}

// simple appel RPC
// to use: driver.rpc('method', 1, 2, 3).then(...)
async rpc (cmd, ...args) {
// envoi une requête rpc
const msgId = this.rpcCalls.length
this.process.send({ msgId, cmd, args })
return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
}

stop () {
this.process.kill()
}
}

module.exports = { TestDriver }

Dans votre code d'application, vous pouvez alors écrire un gestionnaire simple pour recevoir des appels RPC :

main.js
const METHODS = {
isReady () {
// do any setup needed
return true
}
// define your RPC-able methods here
}

const onMessage = async ({ msgId, cmd, args }) => {
let method = METHODS[cmd]
if (!method) method = () => new Error('Invalid method: ' + cmd)
try {
const resolve = await method(...args)
process.send({ msgId, resolve })
} catch (err) {
const reject = {
message: err.message,
stack: err.stack,
name: err.name
}
process.send({ msgId, reject })
}
}

if (process.env.APP_TEST_DRIVER) {
process.on('message', onMessage)
}

Maintenant vous pourrez utiliser votre classe de TestDriver avec le framework d’automatisation de tests de votre choix. L’exemple suivant utilise ava, mais d'autres frameworks comme Jest ou Mocha fonctionneront également :

test.js
const test = require('ava')
const electronPath = require('electron')
const { TestDriver } = require('./testDriver')

const app = new TestDriver({
path: electronPath,
args: ['./app'],
env: {
NODE_ENV: 'test'
}
})
test.before(async t => {
await app.isReady
})
test.after.always('cleanup', async t => {
await app.stop()
})