====== Testing amb Selenium en Node.js ====== Selenium es pot executar en diversos llenguatges, inclòs JS amb Node.js. Ens pot ser molt pràctic per a realitzar tests per a [[Cordova]] o el propi [[NodeJS]]. {{ selenium-diag.png }} Referències: * [[https://www.npmjs.com/package/selenium-webdriver|Selenium Webdriver en NPM]] * [[https://www.w3schools.com/nodejs/ref_assert.asp|NodeJS assert module]] * [[https://www.selenium.dev/documentation/webdriver/interactions/alerts/|Selenium docs for alerts]] * [[https://medium.com/nowports-tech/testing-e2e-con-selenium-web-driver-nodejs-fdd822162fd8|Article d'exemple]] * [[https://www.selenium.dev/blog/2023/headless-is-going-away/|Configurar mode headless en Chrome]]. * [[https://stackabuse.com/executing-shell-commands-with-node-js/|Execute shell commands in NodeJS]]. * [[https://nodejs.org/api/child_process.html|Doc oficial de child_process lib per comandes en NodeJS]]. {{tag> cordova nodejs node javascript }} ===== Basics ===== Si tenim un projecte [[NodeJS]] podem afegir el //packages// amb: $ npm install selenium-webdriver assert child-process Si es tracta d'un projecte en un altre llenguatge podem crear la carpeta ''.test'' i posar els tests a dins, independents del projecte: $ mkdir .test $ cd .test $ npm init $ npm install selenium-webdriver assert child-process Els tests s'executaran entrant a la carpeta ''.test'' i executant-los: $ cd .test $ node 01-xxxx.js Per defecte s'executarà en mode HEADLESS (sense GUI). Si volem veure el //browser//: $ HEADLESS=false node 01-xxx.js Per executar els tests amb Chrome enlloc de Firefox: $ CHROME_TESTS=chrome node 01-xxx.js \\ ===== Projecte base ===== Ens convé tenir els tests independents del codi de l'aplicació. En aquest exemple tenim una estructura amb aquests arxius. Dins ''src'' pot haver el codi en PHP o altres llenguatges que ens interessi: . ├── .gitignore ├── README.md ├── run.sh ├── src │ ├── index.php │ ├── register.php │ └── ... └── .test ├── BaseTest.js └── 01-page-h1.js Cal que **afegim la carpeta ''node_modules/'' al ''.gitignore''** del nostre projecte, encara que no estiguem treballant en NodeJS: node_modules/ \\ ===== Desenvolupament ===== En aquests tests funcionals, els tests estan aïllats del desenvolupament i del llenguatge emprat, pel què podem fer un objecte per testejar qualsevol altre projecte. Només el fitxer ''run.sh'' contindrà les i instruccions adients per engegar un o altre projecte. ==== Arxius de posada en marxa del server ==== ''run.sh'' (o ''run.bat'' en Windows) està a l'arrel del projecte, és a dir, al mateix nivell que la carpeta ''.test'' === run.sh per a PHP === #!/bin/bash # directori del script run.sh SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # entrem a la carpeta del codi font cd $SCRIPT_DIR/src # engeguem el PHP server php -S 0.0.0.0:8000 === run.sh per a Cordova === #!/bin/bash # directori del script run.sh SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # entrem a la carpeta del codi font cd $SCRIPT_DIR # engeguem el cordova sense browser cordova serve === run.sh per a Cordova === cordova serve \\ ==== Llibreria Base ==== const {Builder, Browser, By, Key, until} = require("selenium-webdriver"); const firefox = require('selenium-webdriver/firefox'); const chrome = require('selenium-webdriver/chrome'); const { spawn } = require("child_process"); const assert = require('assert'); class BaseTest { constructor() { console.log("Constructing...") this.headless = process.env.HEADLESS=="false" ? false : true; this.browser = process.env.CHROME_TESTS ? "chrome" : "firefox"; this.cmd = null; this.driver = null; } async setUp() { console.log("HEADLESS:"+this.headless); console.log("BROWSER:"+this.browser); // run server and setup driver await this.runServer( "../run", [] ); await this.setupDriver(); // deixem temps a que el servidor es posi en marxa await this.driver.sleep(2000); } async tearDown() { console.log("Closing server..."); // parem server await this.stopServer(); // deixem temps perquè es tanquin els processos await this.driver.sleep(2000); // tanquem browser console.log("Closing Selenium driver..."); await this.driver.quit(); } async run() { await this.setUp(); try { await this.test(); } finally { await this.tearDown(); } } async test() { console.log("Empty test!"); } async setupDriver() { let firefoxOptions = new firefox.Options(); let chromeOptions = new chrome.Options(); if( this.headless ) { console.log("Running Headless Tests..."); firefoxOptions = new firefox.Options().addArguments('-headless'); chromeOptions = new chrome.Options().addArguments('--headless=new'); } if( this.browser=="chrome" ) { this.driver = await new Builder() .forBrowser(Browser.CHROME) .setChromeOptions(chromeOptions) .build(); } else { this.driver = await new Builder() .forBrowser(Browser.FIREFOX) .setFirefoxOptions(firefoxOptions) .build(); } } runServer( command, options ) { // Engeguem server amb la APP if( process.platform=="win32" ) { this.cmd = spawn(command+".bat",options,{shell:true}); } else { // linux, macos (darwin), or other this.cmd = spawn(command+".sh",options); } this.cmd.stdout.on("data", data => { console.log(`stdout: ${data}`); }); this.cmd.stderr.on("data", data => { console.log(`stderr: ${data}`); }); this.cmd.on('error', (error) => { console.log(`error: ${error.message}`); }); this.cmd.on("close", code => { console.log(`child process exited with code ${code}`); }); } async stopServer() { // tanquem servidor if( process.platform=="win32" ) { spawn("taskkill", ["/pid", this.cmd.pid, '/f', '/t']); } else { // Linux, MacOS or other await this.cmd.kill("SIGHUP") } } } // publiquem l'objecte BaseTest exports.BaseTest = BaseTest; \\ ==== Test 01 : comprovem header H1 ==== // carreguem les llibreries const { BaseTest } = require("./BaseTest.js") const { By, until } = require("selenium-webdriver"); const assert = require('assert'); // heredem una classe amb un sol mètode test() // emprem this.driver per utilitzar Selenium class MyTest extends BaseTest { async test() { // testejem H1 a la home page ////////////////////////////////////////////////////// await this.driver.get("http://localhost:8000/browser/www/"); var currentText = await this.driver.findElement(By.tagName("h1")).getText(); var expectedText = "Tasklist"; assert( currentText==expectedText, "Títol H1 de la pàgina principal incorrecte"); console.log("TEST OK"); } } // executem el test (async function test_example() { const test = new MyTest(); await test.run(); console.log("END") })(); \\ ==== Test 02 : formulari buit ==== Aquest exemple testeja que si deixem buit el nom, ens surt un ''alert'' que ens avisa. // carreguem les llibreries const { BaseTest } = require("./BaseTest.js") const { By, until } = require("selenium-webdriver"); const assert = require('assert'); // heredem una classe amb un sol mètode test() // emprem this.driver per utilitzar Selenium class MyTest extends BaseTest { async test() { // testejem login ////////////////////////////////////////////////////// await this.driver.get("http://localhost:8000/register.php"); //await this.driver.findElement(By.name("nom")).getText(); // el INPUT name="nom" està buit await this.driver.findElement(By.xpath("//button[text()='Seguent']")).click(); // comprovem que l'alert message és ERRONI await this.driver.wait(until.alertIsPresent(),2000,"ERROR TEST: després del SEGUENT ha d'aparèixer un alert amb el resultat de la validació del NOM."); let alert = await this.driver.switchTo().alert(); let alertText = await alert.getText(); let assertMessage = "El NOM no pot estar buit."; assert(alertText==assertMessage,"ERROR TEST: si el nom està buit, l'alert ha de dir: '"+assertMessage+"'."); await alert.accept(); console.log("TEST OK"); } } // executem el test (async function test_example() { const test = new MyTest(); await test.run(); console.log("END") })(); \\ ==== Test Cordova : afegir tasca ==== Aquest exemple inclou l'ús de prompts, que es fa com si fos un alert. Fixeu-vos que Cordova necessita anar a la web ''/browser/www''. // carreguem les llibreries const { BaseTest } = require("./BaseTest.js") const { By, until } = require("selenium-webdriver"); const assert = require('assert'); // heredem una classe amb un sol mètode test() // emprem this.driver per utilitzar Selenium class AddTaskTest extends BaseTest { async test() { // testejem afegir tasca en tasklist de Cordova ////////////////////////////////////////////////////// await this.driver.get("http://localhost:8000/browser/www/"); // cliquem botó "+" await this.driver.findElement(By.xpath("//button[text()='+']")).click(); // el prompt pel text de la tasca es tracta igual que un alert en Selenium await this.driver.wait(until.alertIsPresent(),2000,"ERROR TEST: el botó '+' d'afegir tasca ha d'obrir un prompt."); let prompt = await this.driver.switchTo().alert(); // afegim el text de la tasca i acceptem var taskText = "lalala"; prompt.sendKeys(taskText); await this.driver.sleep(1000); await prompt.accept(); await this.driver.sleep(1000); // checkejem tasca await this.driver.findElement(By.xpath("//li[text()='"+taskText+"']")).click(); console.log("TEST OK"); } } // executem el test (async function test_example() { const test = new AddTaskTest(); await test.run(); console.log("END") })(); Crea un test ''DelTaskTest'' similar a l'anterior que: * Crei 3 tasques amb noms aleatoris. * Esborri una d'elles. * Comprovi que no existeix la tasca esborrada i sí que romanen les altres dues.