Sluit Logo DX Solutions

Automatiseren van serverside website

13 juni 2016 - Xavier
Screenshots met PhantomJS & CasperJS

Een klant wou een grafiek die al op één van de webpagina's stond ook in zijn facturatie pdf's steken. Dat is allemaal wel goed, maar er was een klein probleempje: de grafiek werd door de browser weergegeven met Javascript.

Oplossing aan de browserkant

De grafiek werd weergegeven met behulp van de ChartJS library. Hierdoor gebruikt die het canvas-element om de grafiek in te tekenen. Gelukkig is er een functie in javascript die van een canvas-element de grafische data kan opslaan.

 

var canvas = document.getElementById("graph"); var img_src = canvas.toDataURL();

 

Om de grafiek weer te geven maken we een Chart object aan. Deze heeft ook een andere functie waarmee een afbeelding gemaakt kan worden: toBase64Image()

Jammer genoeg hebben we daarmee onze grafiek nog niet aan de serverkant om in de PDF te steken. We hebben ook niet gekeken naar het automatisch laten versturen (via javascript) van de grafiek naar de server van zodra iemand de pagina bezoekt. Dit omdat we deze voor elk bedrijf aan het einde van de maand moesten genereren. Dus, als die pagina dan niet bezocht werd tussen het einde van de maand en het moment dat we de facturatie-pdf voor die maand maakten, zouden we geen grafiek hebben. Dat is natuurlijk een no-go. Vandaar hebben we deze optie onmiddellijk achterwege gelaten.

 

Oplossing aan de serverkant (PhantomJS & CasperJS)

Dus, om te zorgen dat we aan de serverkant de grafiek hadden om in de pdf te plaatsen, zochten we voor een alternatieve manier om deze op te halen. De volgende waren de 2 meest populaire en voor de hand liggende mogelijkheden:

 

PhantomJS

PhantomJS is een headless internetbrowser (dus zonder grafische interface)  die enkele extra scriptingmogelijkheden toelaat. PhantomJS gebruikt onderliggend een Webkit browser. Je kan het gebruiken om webpagina's te bezoeken, inputvelden in te vullen, screenshots maken, websites 'scrapen' (inlezen om er bepaalde data van op te halen om zelf op te slaan), ...

Bijvoorbeeld, onder dit stukje tekst kan je zien hoe je in Facebook kan inloggen met PhantomJS en een screenshot maken van de nieuwsoverzichtpagina en navigatiebalk.

 

var page = require('webpage').create();

 

We kiezen voor de Safari 7 useragent omdat die ook Webkit gebruikt. Websites die dus de stijl van de pagina aanpassen volgens deze useragent zullen ons hierdoor een correcter resultaat geven.

 

page.settings.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A'; page.settings.javascriptEnabled = true; phantom.cookiesEnabled = true; phantom.javascriptEnabled = true; // We zeggen dat onze browser deze resolutie heeft (voor screenshots) page.viewportSize = { width: 1024, height: 768 } console.log("De hoofdpagina voor facebook.com wordt geopend."); page.open('https://www.facebook.com/', function start(status) { console.log("Aan het controleren of we op de nieuwsoverzichts- of loginpagina zijn."); if (page.title == 'Facebook') { onHomePage(); } else { onLoginPage(); } }); function onLoginPage() { console.log("We zijn op de loginpagina. Begin met inloggen."); page.onLoadFinished = onHomePage; page.evaluate(fillLogin); console.log("Login informatie verzonden"); } var fillLogin = function() { var form = document.getElementById("login_form"); form.elements["email"].value = "username"; form.elements["pass"].value = "password"; form.submit(); } var onHomePage = function() { console.log("We zitten op de nieuwsoverzichtspagina. Bereid je voor om screenshots te maken!"); // We maken een screenshot van de volledige pagina page.clipRect = { top: 0, left: 0, width: page.viewportSize.Width, height: page.viewportSize.Height }; page.render('homepage.png'); // We maken een screenshot van een deel van onze pagina (de navigatiebalk in dit geval) var clipRect = page.evaluate(function() { return document.querySelector("[role=banner]").getBoundingClientRect(); //return document.getElementById("pagelet_bluebar").getBoundingClientRect(); }); page.clipRect = { top: clipRect.top, left: clipRect.left, width: clipRect.width, height: clipRect.height }; page.render("facebook_header.png"); phantom.exit(); }

 

Hierna rest ons enkel het uitvoeren van dit scriptje om een screenshot van de nieuwsoverzichtspagina en navigatiebalk te krijgen.

 

phantomjs --cookies-file=cookies.json facebook_phantom.js

 

Om correct het resultaat te zien moet je mogelijks 2 keer het scriptje uitvoeren, aangezien facebook soms moeilijk kan doen over cookies.

Jammer genoeg zou een simpel scriptje als dit niet voldoende zijn voor onze eerdere usecase om de afbeelding van de grafiek te maken. De grafiek staat namelijk op een AngularJS pagina (een pagina waarbij een deel van de content pas na het initieel laden van de pagina wordt opgehaald). Onze grafiek werd dus ook pas na het initieel laden getoond, nadat een waarde in een selectieveld geselecteerd werd.

De workflow om normaal (zonder scriptje) een screenshot van de grafiek te kunnen zien:

  • We gaan naar de login pagina
  • We geven de logininformatie en en klikken op de verzendknop
  • We gaan naar de pagina die onze grafiek bevat
  • We wachten op angularJS om te laden
  • We selecteren een waarde in het selectieveldje
  • Wanneer onze grafiek zichtbaar is (nadat de data ervoor uit onze API gehaald is), maken we er een screenshot van

Het hoofdprobleem bij het PhantomJS scriptje hier zijn de wachterpioden. We zouden wel dingen kunnen doen zoals

 

window.setTimeout( function() { console.log('Wordt over 1 seconde opgeroepen'); }, 1000 );

 

maar dan is er een ander probleem: als het minder lang duurt dan de tijd die wij ingesteld hebben om de gewenste data op het scherm te krijgen, verliezen we gewoon tijd. Dus, het is best hier een oplossing voor te zoeken.

Nu zou je ook kunnen zeggen "Zet die grafiek gewoon op een aparte pagina zonder AngularJS". Maar we hebben daar niet voor gekozen omdat

  • dezelfde code hergebruikt kon worden voor acceptance testing van het loginformulier en de grafiek (zoals Selenium)
  • geen code duplication

 

CasperJS

Door de nadelen bij PhantomJS besloot ik om nog wat verder te kijken of er geen betere oplossingen waren. Zo kwam ik bij CasperJS terecht, een schil rond PhantomJS die veel voorkomende usecases versimpelt.

Een voorbeeldje hoe we ons vorige PhantomJS scriptje kunnen herschrijven als CasperJS script:

 

var casper = require('casper').create({ pageSettings: { loadPlugins: false, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A' } }); console.log('Open de hoofdpagina van Facebook'); casper.start().thenOpen("https://www.facebook.com/", function() { this.viewport(1024, 768); if (this.exists('#login_form')) { console.log("We zitten op de loginpagina, begin met inloggen."); // Vul de velden in this.fillSelectors('#login_form', { '#email': 'username', '#pass': 'password' }, true); console.log("Logininformatie verzonden"); } }); casper.then(function() { console.log("We zitten op de nieuwsoverzichtspagina! Screenshots aan het voorbereiden."); this.capture('homepage.png'); this.captureSelector("facebook_header.png", '[role="banner"]'); }); casper.run();

 

Wanneer we dit scriptje uitvoeren met het commando

 

casperjs facebook_casper.js

 

krijgen we exact hetzelfde resultaat als het PhantomJS scriptje. De code ziet er direct ook een pak mooier en leesbaarder uit. In tegenstelling tot de PhantomJS versie van het scriptje, moet deze versie slechts 1 keer gedraaid worden.

Leuk, maar met de wijziging van PhantomJS naar CasperJS zal je toch wel meer kunnen dan enkel je scriptje leesbaarder maken zeker? Natuurlijk kan dat! Er zijn enkele functies in CasperJS die ons helpen om beter door AngularJS websites te navigeren.

Aan de website-kant hebben we, om onze grafiek enkel te tonen wanneer er data voor is, een ng-show aan die template tag toegevoegd. Dus, wanneer ik in het scriptje de grafiek probeerde op te halen deed ik eerst een this.fillSelectors om het selectieveldje goed te zetten. Daarna heb ik eerst de this.waitUntilVisiblefunctie geprobeerd om slechts de screenshot te maken wanneer het zichtbaar was.

Maar... dat bleek uiteindelijk niet te werken. Dus besloot ik een andere aanpak te proberen: Wat als we in het scriptje een this.waitForSelector functie combineren met een ng-ifin de template? Hmm, dat lijkt te werken, ideaal!

Je bent eigenlijk niet volledig beperkt tot Webkit browsers: PhantomJS heeft een Firefox alternatief die ook door CasperJS ondersteund wordt, een Internet Explorer alternatief en binnenkort komt er ook een Headless versie van chrome waar we hopelijk binnenkort nog PhantomJS en CasperJS op zullen zien draaien. Dan kunnen we mogelijk zelfs zo ver gaan om hiermee een cross-browser testing platform zoals BrowserStack in te maken.