# 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 !