fr.romain:blog:3.0

Un blog qu'il est bien pour le Java

Faire Du CasperJS en CoffeeScript Sur Windows

| Comments

Windows + CasperJS = Loooove

Je suis revenu de Devoxx France avec quelques idées en tête, dont celle de mettre du CasperJS dans des applications (merci Jean-Laurent et Pierre). Pour faciliter les choses, ma machine tourne sur du Windows (version 7 en l’occurrence), ce qui n’est pas forcément le meilleur environnement pour ce type de chose. Mais on ne va pas se décourager si vite, hein ?

Commençons petit, et essayons de faire marcher un test simple en JavaScript sur CasperJS.

Installation des outils

La première chose à faire, c’est d’installer CasperJS sur la machine Windows. Mais avant cela, il est nécessaire de disposer d’une version assez récente de PhantomJS (CasperJS utilise PhantomJS pour s’exécuter). Donc on télécharge PhantomJS et on l’installe (enfin on décompresse le ZIP). Dans mon cas, il s’agit de la version 1.9. Même chose ensuite, je dézippe le ZIP de CasperJS (v1.0.2), et je finis par ajouter dans mon PATH les chemins vers les exécutables. Voyons si ça marche bien :

1
2
3
4
D:\dev>phantomjs -v
1.9.0
D:\dev>casperjs --version
1.0.2

Les choses s’annoncent bien ! Exécutons un test maintenant. Prennons celui-ci, assez simple :

Le code en version JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var casper = require('casper').create({
    verbose: true,
    logLevel: 'debug'
});

casper.start('https://mon-application:8080/login/', function() {
    this.echo('Log in');
    this.test.assertTitle('Sign in');
    this.test.assertNotVisible('label#error');
    this.fill('form#loginForm', {
        'j_username': 'romain.linsolas',
        'j_password': ''
    }, false);
    this.click("form#loginForm input.button");
    this.test.assertVisible('label.error'); // Une boite d'erreur doit apparaitre
    this.fill('form#loginForm', {
        'j_username': 'romain.linsolas',
        'j_password': 'abc123'
    }, false);
    this.click("form#loginForm input.button");
});

casper.then(function() {
    this.capture('test-screen.png');
    this.test.assertTitle('Hello World'); // Nous sommes loggués
    // Suite du test
});

casper.run(function() {
    this.test.renderResults(true);
});

En gros, il se divise ainsi :

  • J’initialise un contexte casper (dans mon cas, je lui demande d’être un peu pipelette, ça facilitera les choses en cas de problème).
  • Ensuite, je démarre un test où il va exécuter certaines tâches :
    • se connecter à une application ;
    • vérifier le titre de la page ;
    • remplir un formulaire de login sans le mot de passe ;
    • cliquer sur un lien, et vérifier qu’un message d’erreur apparait ;
    • retester en saisissant un mot de passe.
  • Enfin, je vérifie que je suis bien connecté (le titre de la page a changé) et pour le fun, je fais une capture d’écran de la page telle qu’elle est à ce moment-là.

Allez zou, il est temps de tester. Le verdict va tomber très vite, il suffit d’écrire casperjs [mon fichier].js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
D:\dev>casperjs test-casper.js
[info] [phantom] Starting...
[info] [phantom] Running suite: 3 steps
[debug] [phantom] opening url: https://mon-application:8080/login/, HTTP GET
[debug] [phantom] Navigation requested: url=https://mon-application:8080/login/, type=Other, lock=true, isMainFrame=true
[debug] [phantom] url changed to "https://mon-application:8080/login/"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Step 2/3 https://mon-application:8080/login/ (HTTP 200)
Log in
PASS Page title is: "Sign in"
PASS Selector is not visible
[info] [remote] attempting to fetch form element from selector: 'form#loginForm'
[debug] [remote] Set "j_username" field value to romain.linsolas
[debug] [remote] Set "j_password" field value to
[debug] [phantom] Mouse event 'click' on selector: form#loginForm input.button
PASS Selector is visible
[info] [remote] attempting to fetch form element from selector: 'form#loginForm'
[debug] [remote] Set "j_username" field value to romain.linsolas
[debug] [remote] Set "j_password" field value to ******
[debug] [phantom] Mouse event 'click' on selector: form#loginForm input.button
[info] [phantom] Step 2/3: done in 718ms.
[debug] [phantom] Navigation requested: url=https://mon-application:8080/logincheck;jsessionid=808FD9D0EDD9D9C5CCAE7F11F4AE05DE, type=FormSubmitted, lock=true, isMainFrame=true
[debug] [phantom] Navigation requested: url=https://mon-application:8080/, type=FormSubmitted, lock=true, isMainFrame=true
[debug] [phantom] url changed to "https://mon-application:8080/"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Step 3/3 https://mon-application:8080/ (HTTP 200)
[debug] [phantom] Capturing page to D:/dev/test-screen.png
[info] [phantom] Capture saved to D:/dev/.png
PASS Page title is: "Hello World"
[info] [phantom] Step 3/3: done in 1224ms.
[info] [phantom] Done 3 steps in 1226ms

Cool, ça marche !

Passons au CoffeeScript

CoffeeScript

Le JavaScript c’est bien, mais c’est un peu verbeux, surtout quand le code des tests va grandissant. Tentons maintenant de passer à CoffeeScript. CasperJS est l’ami de CoffeeScript et accepte très bien que les scripts à exécuter soient écrits avec. D’après la documentation, il est écrit qu’il suffit de lancer simplement la commande casperjs [mon fichier].coffee. Ca a l’air pas mal. Ecrivons tout d’abord le même code de test, mais cette fois-ci en CoffeeScript. Cela nous donne quelque chose comme ça :

Le code en version CoffeeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
casper = require('casper').create(
    verbose: true
    logLevel: 'debug'
)

casper.start 'https://mon-application:8080/login/', ->
    @echo 'Log in'
    @test.assertTitle 'Sign in'
    @test.assertNotVisible 'label#error'
    @fill 'form#loginForm', {
        j_username: 'romain.linsolas'
        j_password: ''
    }, false
    @click "form#loginForm input.button"
    @test.assertVisible('label.error') ## Error box is displayed
    @fill 'form#loginForm', {
        j_username: 'romain.linsolas'
        j_password: 'abc123'
    }, false
    @click "form#loginForm input.button"

casper.then ->
    @capture 'test-screen.png'
    @test.assertTitle 'Hello World' ## Now we are logged
    ## Suite du test

casper.run ->
    @test.renderResults true

Allez, maintenant on exécute tout ça :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
D:\dev\>casperjs test-casper.coffee
[info] [phantom] Starting...
[info] [phantom] Running suite: 3 steps
[debug] [phantom] opening url: https://mon-application:8080/login/, HTTP GET
[debug] [phantom] Navigation requested: url=https://mon-application:8080/login/, type=Other, lock=true, isMainFrame=true
[debug] [phantom] url changed to "https://mon-application:8080/login/"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Step 2/3 https://mon-application:8080/login/ (HTTP 200)
Log in
PASS Page title is: "Sign in"
PASS Selector is not visible
[info] [remote] attempting to fetch form element from selector: 'form#loginForm'
[debug] [remote] Set "j_username" field value to romain.linsolas
[debug] [remote] Set "j_password" field value to
[debug] [phantom] Mouse event 'click' on selector: form#loginForm input.button
PASS Selector is visible
[info] [remote] attempting to fetch form element from selector: 'form#loginForm'
[debug] [remote] Set "j_username" field value to romain.linsolas
[debug] [remote] Set "j_password" field value to ******
[debug] [phantom] Mouse event 'click' on selector: form#loginForm input.button
[info] [phantom] Step 2/3: done in 1099ms.
[debug] [phantom] Navigation requested: url=https://mon-application:8080/logincheck;jsessionid=0A26317CDA05D97D63D6538EE4212B07, type=FormSubmitted, lock=true, isMainFrame=true
[debug] [phantom] Navigation requested: url=https://mon-application:8080/, type=FormSubmitted, lock=true, isMainFrame=true
[debug] [phantom] url changed to "https://mon-application:8080/"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Step 3/3 https://mon-application:8080/ (HTTP 200)
PASS Page title is: "Hello World"
[debug] [phantom] Mouse event 'click' on selector: xpath selector: //*[text()="Administration"]
[debug] [phantom] Navigation requested: url=https://mon-application:8080/domain/account/index, type=LinkClicked, lock=true, isMainFrame=true
[info] [phantom] Step 3/3: done in 1389ms.
[debug] [phantom] url changed to "https://mon-application:8080/domain/account/index"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Done 3 steps in 1749ms

Voilà qui est génial, CasperJS gère nativement le CoffeeScript sans avoir besoin de l’installer séparément. Mais on va quand même procéder à son installation, comme ça on disposera d’un compilateur CoffeeScript.

La première étape, c’est l’installation de NodeJS. Les choses se font assez simplement, il y a un .msi à télécharger. Il me faudra juste ajouter le chemin d’installation de NodeJS dans mon PATH :

1
2
3
4
D:\dev>node -v
v0.10.3
D:\dev>npm -v
1.2.17

Maintenant, installons CoffeeScript grâce à la commande npm install -g coffee-script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
D:\dev\nodejs>npm install -g coffee-script
npm http GET https://registry.npmjs.org/coffee-script
npm http GET https://registry.npmjs.org/coffee-script
npm http GET https://registry.npmjs.org/coffee-script
npm ERR! Error: getaddrinfo ENOTFOUND
npm ERR!     at errnoException (dns.js:37:11)
npm ERR!     at Object.onanswer [as oncomplete] (dns.js:124:16)
npm ERR! If you need help, you may report this log at:
npm ERR!     <http://github.com/isaacs/npm/issues>
npm ERR! or email it to:
npm ERR!     <npm-@googlegroups.com>
npm ERR! System Windows_NT 6.1.7601
npm ERR! command "D:\\dev\\nodejs\\\\node.exe" "D:\\dev\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "install" "-g" "coffee-script"
npm ERR! cwd D:\dev\nodejs
npm ERR! node -v v0.10.3
npm ERR! npm -v 1.2.17
npm ERR! syscall getaddrinfo
npm ERR! code ENOTFOUND
npm ERR! errno ENOTFOUND
npm ERR!
npm ERR! Additional logging details can be found in:
npm ERR!     D:\dev\nodejs\npm-debug.log
npm ERR! not ok code 0

Comme les choses ne sont pas simples chez moi, je suis derrière un proxy :) Il faut donc donner les informations de connexion à npm pour se connecter à Internet. Cela se fait dans le fichier <répertoire home>\.npmrc (par exemple C:\Users\chuck.norris\.npmrc). Dans ce fichier, on définit l’adresse des proxies ainsi que le registre npm :

1
2
3
proxy = http://[user]:[mot de passe]@[url proxy]:[port]/
https-proxy = http://[user]:[mot de passe]@[url proxy]:[port]/
registry = http://registry.npmjs.org

On relance la même commande, et cette fois-ci ça marche mieux :

1
2
3
4
5
6
7
8
D:\dev\nodejs>npm install -g coffee-script
npm http GET http://registry.npmjs.org/coffee-script
npm http 200 http://registry.npmjs.org/coffee-script
npm http GET http://registry.npmjs.org/coffee-script/-/coffee-script-1.6.2.tgz
npm http 200 http://registry.npmjs.org/coffee-script/-/coffee-script-1.6.2.tgz
C:\Users\chuck.norris\AppData\Roaming\npm\cake -> C:\Users\chuck.norris\AppData\Roaming\npm\node_modules\coffee-script\bin\cake
C:\Users\chuck.norris\AppData\Roaming\npm\coffee -> C:\Users\chuck.norris\AppData\Roaming\npm\node_modules\coffee-script\bin\coffee
coffee-script@1.6.2 C:\Users\chuck.norris\AppData\Roaming\npm\node_modules\coffee-script

Cool ! Voyons maintenant s’il est possible de lancer la commande coffee :

1
2
D:\dev>coffee -v
CoffeeScript version 1.6.2

Houra !

Intégration au build

Dernière étape de ce petit tutoriel : comment allons-nous faire pour exécuter automatiquement les tests CoffeeScript, pour les intégrer par exemple dans notre build continu sur Jenkins ? On pourrait effectivement ajouter une étape dans la configuration Jenkins qui irait lancer un script, mais ce n’est pas très propre.

J’ai trouvé quelques plugins Maven, mais rien de folichon. J’ai donc décidé d’en créer un moi-même, mais il n’est pas encore stabilisé.

En attendant, je vais passer par un test JUnit, qui aura pour tâche de lister les *.js et *.coffee d’un répertoire donné. C’est une version très simple, elle ne gère que les cas standards (j’essaierais de faire les choses plus proprement dans mon plugin Maven). Elle ne supporte pas non plus les options de la commande casperjs (comme par exemple les --pre ou --post), mais c’est un premier pas et surtout, ça marche :)

Classe JUnit d’exécution des fichiers JavaScript et CoffeeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package fr.linsolas.javascript.casperjs;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.lang.StringUtils;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;

import static org.fest.assertions.Assertions.assertThat;

/**
 * Run CasperJS on any *.js or *.coffee files found in a specific folder.
 * @author Romain Linsolas
 */
public class RunCasperJSTest {

    private static boolean casperFound = false;
    private static final String CASPER_JS = "casperjs.bat %s";
    private static final String TESTS_DIR = "D:\\dev\\mon-application\\src\\test\\javascript\\casperjs";

    @BeforeClass
    public static void checkEnvironment() {
        // Vérification que l'on a bien CasperJS installé.
        int res = execute(CASPER_JS, "--version");
        assertThat(res).isNotEqualTo(-1);
        casperFound = true;
    }

    @Test
    public void runJavaScriptTests() {
        File[] files = new File(TESTS_DIR).listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return StringUtils.endsWithIgnoreCase(name, ".js");
            }
        });
        runTests(files);
    }

    @Test
    public void runCoffeeScriptTests() {
        File[] files = new File(TESTS_DIR).listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return StringUtils.endsWithIgnoreCase(name, ".coffee");
            }
        });
        runTests(files);
    }

    private void runTests(File[] files) {
        assertThat(casperFound).isTrue();
        int ok = 0;
        int ko = 0;
        for (File f : files) {
            int res = execute(CASPER_JS, f.getAbsolutePath());
            if (res == 0) { ok++; } else { ko++; }
        }
        System.out.println("Results: " + ok + " test(s) successful, " + ko + " test(s) failed. Total of " + (ok + ko) + " test(s).");
        assertThat(ko).isEqualTo(0);
    }


    private static int execute(String command, Object... arguments) {
        DefaultExecutor exec = new DefaultExecutor();
        CommandLine line = CommandLine.parse(String.format(command, arguments));
        try {
            return exec.execute(line);
        } catch (IOException e) {
            return -1;
        }
    }

}

Logo Windows>TM par YOOTheme, image de coeur par David Vignoni

Comments