====== 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.