# Formation CasperJS
## CasperJS - navigateur headless - basé sur * [PhantomJS] — moteur [WebKit] ([QtWebKit]) * [SlimerJS] - moteur [Gecko] plus récent - moteur JavaScript Qt (JavaScriptCore) [PhantomJS]: http://phantomjs.org/ [SlimerJS]: http://slimerjs.org/ [WebKit]: http://webkit.org/ [Gecko]: http://fr.wikipedia.org/wiki/Gecko_%28moteur_de_rendu%29 [QtWebKit]: https://qt-project.org/wiki/QtWebKit
## Usages principaux - *Scraping* - Tests fonctionnels
## Scraping - récupérer des informations depuis des pages web - difficultés: * pages et webapps utilisant massivement JavaScript * automatisation * **navigation et interactions**
## Tests fonctionnels - tester la conformité du comportement d'une application Web - difficultés: * pages et webapps utilisant massivement JavaScript * automatisation * **navigation et interactions**
## CasperJS > CasperJS permet de scripter des scenarios automatisés de navigation et > d'interaction avec des applications Web utilisant massivement JavaScript.
## Confusions fréquentes - **CasperJS n'est pas un package NodeJS** - [PhantomJS] (et QtWebkit) n'utilisent **pas** le moteur V8 de Google, mais [JavaScriptCore], le moteur natif de WebKit * les versions de WebKit et JavaScriptCore utilisées par PhantomJS sont vieillissantes - [SlimerJS] utilise lui le moteur JavaScript plus actuel des versions récentes de Firefox * SlimerJS n'est pas *headless*, il utilise [Xvfb] pour émuler un buffer graphique (il est par conséquent un peu plus lent que PhantomJS) [PhantomJS]: http://phantomjs.org/ [SlimerJS]: http://slimerjs.org/ [JavaScriptCore]: http://trac.webkit.org/wiki/JavaScriptCore [Xvfb]: http://en.wikipedia.org/wiki/Xvfb
# Installation
## Installation (OSX) Utiliser [Homebrew], un gestionnaire de paquet pour OS X, est la solution la plus simple : ``` $ ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)" $ brew update $ brew install casperjs --devel $ casperjs --version 1.1-beta2 ``` **Note :** PhantomJS sera alors installé en tant que dépendance. [Homebrew]: http://mxcl.github.io/homebrew/
## Installation (Linux) PhantomJS s'installe facilement via [npm], tandis que CasperJS s'installera via `git`: ``` $ npm install -g phantomjs $ git clone https://github.com/n1k0/casperjs.git $ cd casperjs $ ln -sf `pwd`/bin/capserjs /usr/local/bin/casperjs $ git co 1.1-beta2 $ casperjs --version 1.1-beta2 ``` [npm]: https://npmjs.org/
## Installation (Windows) - Téléchargez la version `1.1-beta2 sur [github] - Décompressez l'archive vers `C:\casperjs` - Ajoutez `;C:\casperjs\bin` à votre variable d'environnement `PATH` Vous pouvez désormais utiliser la commande `casperjs.exe` : C:> casperjs.exe myscript.js **Notes :** - le support de Windows est assuré par la communauté de contributeurs CasperJS - il est recommandé d'utiliser CasperJS dans un environnement \*n*x [github]: https://github.com/n1k0/casperjs/releases
## Premier script Dans un nouveau fichier `casper-commits.js` : ```js require("casper").create({pageSettings: {loadImages: false, loadPlugins: false}}) .start("https://github.com/n1k0", function() {this.clickLabel("casperjs")}) .waitForSelector(".commits a").thenClick(".commits a") .waitForSelector(".commit-group-heading").run(function() { this.echo("Latest commits from CasperJS:\n\n" + this.evaluate(function() { var nodes = document.querySelectorAll('.commit-title'); return [].map.call(nodes, function(node) { return node.textContent.replace("…", "").trim(); }); }).join("\n - ")).exit(); }); ```
## Cela nous donne ``` $ casperjs casper-commits.js Latest commits from CasperJS: - fixed jshint issue - updated obsolete doclinks in README - fixes #704 - added --auto-exit=no test runner option - added missing test file - refs #721 - refs #546 - even better 1.1 strategy for storing successes/failures - fixes #704 - provide an 'exit' tester event to hook before runner exits - refs #546 - documented fallback strategies for getPasses/getFailures - fixes #546 - updated 1.1 upgrade docs regarding getPasses/getFailures - minor docs fixes for clientutils - Merge pull request #718 from phillipalexander/fix-readme-typo … ```

Des questions ?

Oui, c'était une blague.

Mais à la fin vous pourrez produire vous-même ce genre de script

(En beaucoup plus beau)

# Quelques rappels sur JavaScript
## Scope ```js function hello() { var target = "world"; return "hello, " + target; } console.log(hello()) // "hello, world" console.log(target) // ReferenceError: target is not defined ```
## Scope ```js function hello() { target = "world"; return "hello, " + target; } console.log(hello()) // "hello, world" console.log(target) // "world" ``` Ici, l'absence d'utilisation du mot-clé `var` stocke `target` dans le scope global, c'est à dire attache la propriété `target` à l'objet `window`.
## Scope ```js function hello() { "use strict"; target = "world"; return "hello, " + target; } console.log(hello()) // ReferenceError: assignment to undeclared variable target console.log(target) // undefined ``` La directive `"use strict"` effectue une vérification stricte du contexte dans lequel nous déclarons nos variables. Utilisez-la aussi souvent que possible!
## Scope ```js function hello(target) { function capitalize() { return target.toUpperCase(); } return "hello, " + capitalize(); } console.log(hello("chuck")) // "hello, CHUCK" console.log(target) // undefined ``` Un fonction imbriquée accède au scope de sa fonction parente. Les arguments reçus par une fonction ne sont pas exposés hors de son scope.
## Scope ```js function hello(target) { function capitalize() { target = target.toUpperCase(); } capitalize(); return "hello, " + target; } console.log(hello("chuck")) // "hello, CHUCK" ``` Une fonction imbriquée peut altérer une variable du scope parent, mais cette pratique est vivement déconseillée, car ce type d'effets de bords sont toujours plus difficiles à contrôler et à tester.
## Closures ```js function add(x) { return function(y) { return x + y; }; } var add5 = add(5); console.log(add5(3)); // 8 console.log(add(5)(3)); // 8 ``` Une closure est une fonction qui effectue des opérations sur ou à partir des éléments de son scope parent. Ici, la fonction `add` prend un argument `x` et retourne elle-même une autre fonction prenant un paramètre `y` qui sera ajouté à `x`. L'aspirine est dans le petit placard blanc de la salle de bain.
## Closures ```js var functions = []; for (var i = 0; i < 10; i++) { functions.push(function() { console.log(i); }); } functions.forEach(function(fn) { fn(); }); ``` Cela affiche dix fois le nombre `10`. Que s'est-il passé ?
## Closures ```js var functions = []; for (var i = 0; i < 10; i++) { functions.push(function(i) { return function() { console.log(i); }; }(i)); } functions.forEach(function(fn) { fn(); }); ``` Grace à une closure, nous avons isolé la valeur de `i` utilisée par chacune des fonctions de notre tableau de fonctions.
## IIFE ```js var functions = []; for (var i = 0; i < 10; i++) { (function(i) { functions.push(function() { console.log(i); }); })(i); } functions.forEach(function(fn) { fn(); }) ``` Ici nous utilisons une IIFE — Immediately Invoked Function Expression — qui aboutit au même résultat.
## Types ```js >>> typeof "foo" ??? >>> typeof null ??? >>> typeof NaN ??? >>> typeof new Date() ??? >>> Object.prototype.toString.call(new Date()) ??? ```
## Types ```js >>> typeof "foo" "string" >>> typeof null object >>> typeof NaN "number" >>> typeof new Date() "object" >>> Object.prototype.toString.call(new Date()) "[object Date]" ```
## Prototype ```js >>> function Cow(name) { this.name = name; } >>> Cow.prototype.say = function(what) { console.log(this.name + ' says ' + what); }; >>> var cow = new Cow('Geneviève'); >>> cow.say('Meuh.'); // => "Geneviève says Meuh." ```
## Sérialisation ```js >>> var cow = new Cow('Geneviève'); [Object object] >>> cow = JSON.parse(JSON.stringify(cow)); [Object object] >>> cow.says('Halp.'); ??? ```
## Sérialisation ```js >>> var cow = new Cow('Geneviève'); [Object object] >>> cow = JSON.parse(JSON.stringify(cow)); [Object object] >>> cow.says('Halp.'); TypeError: cow.says is not a function ``` Un objet cloné de cette façon renverra toujours un objet natif dépossédé de toutes les méthodes que vous y avez ajouté.
## `Array#forEach` ```js [1, 2, 3].forEach(function(i) { console.log(i); }); // => 1 // => 2 // => 3 ```
## `Array#map` ```js [1, 2, 3].map(function(x) { return x * 2; }); // [2, 4, 6] ```
## `Array#reduce` ```js [1, 2, 3].reduce(function(acc, x) { return acc + x; }); // 6 ```
## `Array#filter` ```js [1, 2, 3].filter(function(x) { return x > 1 && x < 3; }); // [2] ```
## `Arguments` ```js function adder() { return [].reduce.call(arguments, function(acc, x) { return acc + x; }); } adder(1, 2) // 3 adder(1, 2, 3) // 6 ```
## `Function#apply` ```js function adder() { return [].reduce.call(arguments, function(acc, x) { return acc + x; }); } adder.apply(null, [1, 2]) adder.apply(null, [1, 2, 3]) ```
## `Function#call` ```js function adder() { return this.reduce(function(acc, x) { return acc + x; }); } adder.call([1, 2]) adder.call([1, 2, 3]) ```
## `Function#bind` ```js function test() { "use strict"; console.log(this, JSON.stringify(arguments)); } >>> test(); undefined '{}' >>> test.bind("foo")(); foo {} >>> test.bind("foo", 1, 2)(); foo {"0":1,"1":2} ```
# Rappels sur le DOM
## `document.querySelector` ```js >>> typeof document.querySelector('body') ??? >>> document.querySelector('body') instanceof HTMLBodyElement ??? >>> document.querySelector('body') instanceof HTMLElement ??? >>> document.querySelector('#nonexistent') ??? >>> typeof document.querySelector('#nonexistent') ??? ```
## `document.querySelector` ```js >>> typeof document.querySelector('body') "object" >>> document.querySelector('body') instanceof HTMLBodyElement true >>> document.querySelector('body') instanceof HTMLElement true >>> document.querySelector('#nonexistent') null >>> typeof document.querySelector('#nonexistent') "object" ```
## `document.querySelectorAll` ```js >>> typeof document.querySelectorAll('a') ??? >>> document.querySelectorAll('a') instanceof HTMLElement ??? >>> document.querySelectorAll('a') instanceof NodeList ??? >>> document.querySelectorAll('a')[0] instanceof HTMLElement ??? >>> document.querySelectorAll('a') instanceof Array ??? ```
## `document.querySelectorAll` ```js >>> typeof document.querySelectorAll('a') "object" >>> document.querySelectorAll('a') instanceof HTMLElement false >>> document.querySelectorAll('a') instanceof NodeList true >>> document.querySelectorAll('a')[0] instanceof HTMLElement true >>> document.querySelectorAll('a') instanceof Array false ```
## `NodeList` ```js >>> document.body.innerHTML = '<ul><li>1</li><li>2</li><li>3</li></ul>' ``` ```js >>> document.querySelectorAll('li').length ??? ``` ```js >>> document.querySelectorAll('li').map(function(node) { >>> return parseInt(node.textContent, 10); >>> }) ??? ```
## `NodeList` ```js >>> document.body.innerHTML = '<ul><li>1</li><li>2</li><li>3</li></ul>' ``` ```js >>> document.querySelectorAll('li').length 3 ``` ```js >>> document.querySelectorAll('li').map(function(node) { >>> return parseInt(node.textContent, 10); >>> }) TypeError: document.querySelectorAll(...).map is not a function ```
## `NodeList` La solution, utiliser la fonction prototypale `Array#map` en lui passant le contexte approprié : ```js >>> Array.prototype.map.call(document.querySelectorAll('li'), function(node) { >>> return parseInt(node.textContent, 10); >>> }) [1, 2, 3] ``` Ou encore : ```js >>> [].map.call(document.querySelectorAll('li'), function(node) { >>> return parseInt(node.textContent, 10); >>> }) [1, 2, 3] ```
# Scraping
## Scraping > Le Web scraping (parfois appelé Harvesting) est une technique d'extraction > du contenu de sites Web, via un script ou un programme, dans le but de le > transformer pour permettre son utilisation dans un autre contexte — [Wikipedia](http://fr.wikipedia.org/wiki/Web_scraping)
## Scraping: exemple 1 **Extraire tous les liens depuis la page [fr.wikipedia.org/wiki/Web_Scraping](http://fr.wikipedia.org/wiki/Web_scraping).**
## Commençons doucement… Créer un fichier `exercices/1/1.js` : ```js // exercices/1/1.js var casper = require('casper').create(); casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() { this.debugHTML(); }); casper.run(); ``` Lancer le script : ``` $ casperjs exercices/1/1.js ```
## Que s'est il passé ? On crée un object `casper` : ```js var casper = require('casper').create(); ``` *Notez l'utilisation du pattern module, comme dans node.js*
## Que s'est il passé ? On lance le navigateur sur l'url : ```js casper.start('http://fr.wikipedia.org/wiki/Web_scraping'); ``` *Considérez la méthode `start()` comme l'ouverture d'un nouvel onglet et le chargement d'une url dans un navigateur classique.*
## Que s'est il passé ? On définit le comportement à la réception de la réponse HTTP attendue ; ici, on affiche simplement le contenu HTML brut de la page : ```js casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() { this.debugHTML(); }); ``` *La méthode `debugHTML()` est très utile pour savoir exactement quel contenu HTML est utilisé dans la page courante.*
## Que s'est il passé ? Enfin, on lance le traitement de l'ensemble des opérations programmées : ``` casper.run(); ``` *Une erreur fréquente des débutants est d'omettre l'appel à `run()`, résultant sur un script figé.*
## Asynchronicité On pourrait aussi écrire : ```js var casper = require('casper').create(); casper.start('http://fr.wikipedia.org/wiki/Web_scraping'); casper.then(function() { this.debugHTML(); }); casper.run(); ```
## Asynchronicité Ou encore : ```js require('casper') .create() .start('http://fr.wikipedia.org/wiki/Web_scraping') .then(function() { this.debugHTML(); }) .run(); ``` Plus une affaire de goûts et de couleurs qu'autre chose…
## Extraction des liens ```js // exercices/1/2.js var casper = require('casper').create(); var links = []; casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() { links = this.evaluate(function() { var nodes = document.querySelectorAll('a'); return [].map.call(nodes, function(node) { return node.getAttribute('href'); }); }); }); casper.run(function() { this.echo(links).exit(); }); ```
## Hmm, WAT? - oui, ça fait toujours ça la première fois quand on est pas habitué - une grosse partie de la *magie* réside dans la méthode `evaluate()` - une grosse partie des problèmes rencontrés par les débutants aussi, d'ailleurs Regardons plus en détail ce qui s'est passé…
## Que s'est il passé ? Une fois la page Wikipedia ouverte, nous appelons la méthode `evaluate()` depuis le context casper afin de récupérer une valeur depuis le DOM de la page en question : ```js var links = []; casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() { links = this.evaluate(function() { // ce qui sera retourné ici sera affecté à la variable links }); }); ```
## Que s'est il passé ? Et que voulons-nous affecter à notre variable `links` ? La liste des liens hypertextes de la page courrante ; aussi nous commençons par récupérer la liste des éléments DOM correspondants : ```js var nodes = document.querySelectorAll('a'); ``` Que nous mappons afin d'en extraire le contenu de l'attribut `href` : ```js return [].map.call(nodes, function(node) { return node.getAttribute('href'); }); ``` **Note:** *Nous ne pouvons pas directement mapper la liste d'éléments car le type `NodeList` n'implémente pas complètement le prototype `Array` ; nous appliquons donc directement la méthode `Array#map` à notre objet `NodeList` au moyen de `Function#call`.*
## Que s'est-il passé ? Enfin, nous lançons l'exécution des opérations et affichons le contenu de notre variable `links` : ```js casper.run(function() { this.echo(links).exit(); }); ```
## Notes sur `Casper#evaluate()` Pour évaluer du code dans le contexte de la page Web que vous scrapez, il faut utiliser la méthode [Casper#evaluate()]. **Par sécurité, cette fonction est *sandboxée***, il n'y a aucun moyen pour le code qui lui est passé d'accéder aux objets et variables JavaScript en dehors du contexte de la page en question. Comme [pour PhantomJS](https://github.com/ariya/phantomjs/wiki/Quick-Start#code-evaluation), **l'utilisation du *sandboxing* implique de ne pouvoir transférer que des objets sérializables** de l'environnement JavaScript de la page à l'environnement d'exécution CasperJS. **Astuce :** considérez une valeur comme sérializable lorsque l'expression suivante est vraie : ```js value === JSON.parse(JSON.stringify(value)) // true ``` [Casper#evaluate()]: http://casperjs.org/api.html#casper.evaluate
## Notes sur `Casper#evaluate()` Ceci fonctionnera : ```js var href = casper.evaluate(function() { return document.querySelector('a').href; // String }); ``` Ceci ne fonctionnera pas : ```js var href = casper.evaluate(function() { return document.querySelector('a'); // HTMLElement, non serializable }); ```
## Notes sur `Casper#evaluate()` Ce diagramme illustre le principe d'étanchéïté entre les deux environnements : ![](img/evaluate-diagram.png)
## Notes sur `Casper#evaluate()` Une fonction passée à `evaluate()` n'a pas accès au scope de l'environnement CasperJS ; ceci ne fonctionnera pas : ```js var toto = 42; this.echo(this.evaluate(function() { return toto; // undefined })); ``` On peut cependant passer des arguments inline à `evaluate()` : ```js var toto = 42; this.echo(this.evaluate(function(toto) { return toto; // 42 }, toto)); ```
## Un peu plus dur… Vous connaissez sans doute la fonctionnalité de Google *Suggest*, qui permet de proposer des suggestions pour un terme de recherche particulier : ![](img/suggest.png) Allons scraper tout ça pour rigoler un coup (ou pas)
## Un peu plus dur… L'idée est de développer un script permettant, pour un terme de recherche passé, d'afficher la liste des suggestions correspondantes depuis la ligne de commande : ``` $ casperjs google-suggest.js pourquoi pourquoi la guerre au mali pourquoi pas coline pourquoi je me suis marié pourquoi le ciel est bleu pourquoi la france intervient au mali pourquoi je me suis marié aussi pourquoi j'ai mangé mon père pourquoi on baille pourquoi le pape démissionne ```
## Quelques indices Certains outils peuvent également s'avérer utiles : - le module [cli] vous permet facilement de récupérer les arguments passés en ligne de commande ; - [Casper#sendKeys()] permet de simuler la saisie d'un texte sur un champ de formulaire ; - [Casper#waitFor] permet de déferrer l'execution de l'étape suivante au moment où certaines conditions sont réunies ; - [Casper#fetchText()] permet de récupérer le contenu textuel d'un élement correspondant à un selecteur donné. [cli]: http://casperjs.org/cli.html [Casper#fetchText()]: http://casperjs.org/api.html#casper.fetchText [Casper#sendKeys()]: http://casperjs.org/api.html#casper.sendKeys [Casper#waitFor]: http://casperjs.org/api.html#casper.waitFor
# C'est à vous !
## Récupérer un argument Une fois l'objet `casper` créé, il dispose d'un attribut `cli` qui permet de récupérer les informations passées en ligne de commande ; ici, nous souhaitons récupérer le premier argument : ```js var casper = require('casper').create(); var word = casper.cli.get(0); ```
## Remplir un champ La méthode [Casper#sendKeys()] permet de simuler la saisie d'un texte sur un champ de formulaire ; Nous allons saisir le mot passé en argument du script dans la zone de recherche google : ```js casper.start('http://www.google.com/', function() { this.sendKeys('input[name=q]', word); }); ``` [Casper#sendKeys()]: http://casperjs.org/api.html#casper.sendKeys
## Attendre l'autocomplétion L'affichage des suggestions de recherche Google étant asynchrone, nous utilisons la méthode [Casper#waitFor] afin d'attendre que le sélecteur correspondant existe et commence par le mot que nous recherchons ; ici, le sélecteur CSS3 correspondant est `.gsq_a table span` : ```js casper.waitFor(function() { return this.fetchText('.gsq_a table span').indexOf(word) === 0 }, function() { // la boite d'autocomplétion est disponible // récupération des suggestions }); ``` [Casper#waitFor]: http://casperjs.org/api.html#casper.waitFor
## Récupérer les suggestions Une fois la zone affichant les suggestions est visible, nous pouvons procéder à la récupération des textes pour chaque élément de la liste correspondante : ```js suggestions = this.evaluate(function() { var nodes = document.querySelectorAll('.gsq_a table span'); return [].map.call(nodes, function(node){ return node.textContent; }); }); ```
## Affichage des suggestions Enfin, nous affichons la liste des résultats que nous avons stocké : ```js casper.run(function() { this.echo(suggestions.join('\n')).exit(); }); ```
# Tests fonctionnels
## Tests fonctionnels Utiles pour : - garantir l'intégrité fonctionnelle d'une application - valider un comportement attendu - garantir contre les régressions éventuelles Le framework de test fonctionnel a été refondu en version 1.1, et propose désormais des fonctionnalités avancées de reporting, les méthodes `setUp()` et `tearDown()` et propose une architecture plus découplée. La documentation de la version de développement est située à l'adresse [docs.casperjs.org](https://docs.casperjs.org/).
## Installation de la version 1.1-DEV Installer la version 1.1 de développement : ``` $ git clone https://github.com/n1k0/casperjs.git $ cd casperjs $ git co master $ casperjs --version 1.1.0-DEV ```
## La sous-commande `casperjs test` L'utilisation des fonctionnalités de test implique l'utilisation d'une sous-commande particulière, `casperjs test` : ``` $ casperjs test mytest.js ```
## Premier script de test Testons la recherche google : ```js // exercices/3/1.js casper.test.begin('Google search tests', 4, function suite(test) { casper.start("http://www.google.fr/", function() { test.assertTitle("Google", "google homepage title is the one expected"); test.assertExists('form[action="/search"]', "main form is found"); this.fill('form[action="/search"]', { q: "casperjs" }, true); }); casper.waitFor(function() { return this.getTitle() === "casperjs - Recherche Google"; }, function() { test.assertUrlMatch(/q=casperjs/, "search term has been submitted"); test.assertEval(function() { return document.querySelectorAll("h3.r").length >= 10; }, "google search for \"casperjs\" retrieves 10 or more results"); }); casper.run(function() { test.done(); }); }); ```
## Execution Vous devriez obtenir quelque chose d'approchant : ![](img/test-results.png)
## Execution En faisant volontairement échouer le test : ![](img/test-results-fail.png)
## Que s'est-il passé ? ```js casper.test.begin('Google search tests', 4, function suite(test) { ``` Nous démarrons un nouveau test en spécifiant : 1. son nom, ici *Google search tests* 2. le nombre d'assertions attendues, ici *4* 3. une fonction définissant les assertions à executer; notez l'argument `test` contenant une référence à l'objet [Tester]. [Tester]: http://docs.casperjs.org/en/latest/modules/tester.html
## Que s'est-il passé ? ```js casper.start("http://www.google.fr/", function() { test.assertTitle("Google", "google homepage title is the one expected"); test.assertExists('form[action="/search"]', "main form is found"); this.fill('form[action="/search"]', { q: "casperjs" }, true); }); ``` Nous ouvrons la page d'accueil de Google, puis : 1. vérifions le titre de la page (le contenu de sa balise `<title>`) 2. vérifions que le formulaire de recherche existe, à partir d'un sélecteur CSS3 3. remplissons le champ de recherche avec le mot-clé *casperjs* et déclenchons sa soumission
## Que s'est-il passé ? ```js casper.waitFor(function() { return this.getTitle() === "casperjs - Recherche Google"; }, function() { test.assertUrlMatch(/q=casperjs/, "search term has been submitted"); test.assertEval(function() { return document.querySelectorAll("h3.r").length >= 10; }, "google search for \"casperjs\" retrieves 10 or more results"); }); ``` Nous attendons que le titre de la page ait changé, puis nous vérifions : 1. que l'URL de la page a changé conformément 2. que le nombre de résultats de recherche est supérieur à 10
## Que s'est-il passé ? ```js casper.run(function() { test.done(); }); ``` Enfin, nous executons l'ensemble des opérations définies et appelons la méthode [Tester#done()] afin de signifier que le test est terminé. [Tester#done()]: http://docs.casperjs.org/en/latest/modules/tester.html#done
## Export au format XUnit L'option `--xunit` de la commande `casper test` permet d'exporter le résultat d'une suite de tests au format *XUnit* : ```js $ casperjs test exercices/3/1.js --xunit=log.xml ``` Ces logs dont utiles dans la perspective de la mise en place d'[intégration continue] avec un serveur comme [Jenkins]. [intégration continue]: https://fr.wikipedia.org/wiki/Int%C3%A9gration_continue [Jenkins]: https://fr.wikipedia.org/wiki/Jenkins_(informatique)
## Export au format XUnit Le fichier `log.xml` contient alors : ```xml <?xml version="1.0" encoding="UTF-8"?> <testsuites duration="0.94"> <testsuite errors="0" failures="0" name="Google search tests" package="exercices/3/1" tests="4" time="0.94" timestamp="2013-04-29T16:12:01.211Z"> <testcase classname="exercices/3/1" name="google homepage title is the one expected" time="0.55"/> <testcase classname="exercices/3/1" name="main form is found" time="0.001"/> <testcase classname="exercices/3/1" name="search term has been submitted" time="0.388"/> <testcase classname="exercices/3/1" name="google search for 'casperjs' retrieves 10 or more results" time="0.001"/> <system-out/> </testsuite> </testsuites> ```
## Outils & ressources - la [documentation de la branche 1.0.x de CasperJS](http://casperjs.org/) - la [documentation de la branche 1.1.x de CasperJS](http://docs.casperjs.org/) - le [bookmarklet] CasperJS, pour injecter les utilitaires client dans la page courante - [Arora], un navigateur basé sur QtWebKit dont le rendu et le comportement sont extrêmement proches de ceux de PhantomJS, et donc de CasperJS - [resurectio], une extension Chrome pour enregistrer vos scénarios de navigation et les exporter en scripts CasperJS - l'[organisation casper sur github](https://github.com/casperjs) qui liste une grande partie de l'ecosystème CasperJS [bookmarklet]: http://casperjs.org/api.html#bookmarklet [Arora]: https://code.google.com/p/arora/ [resurectio]: https://github.com/ebrehault/resurrectio
# Merci !

/

#