Nicolas Perriault https://nicolas.perriault.net/ Nicolas Perriault's personal website. en-us Mon, 29 Apr 2019 09:30:54 +0000 Mon, 29 Apr 2019 09:30:54 +0000 Copyright 2010-2013 Nicolas Perriault https://github.com/n1k0/nicolas.perriault.net nicolas+rss@perriault.net (Nicolas Perriault) nicolas+rss@perriault.net (Nicolas Perriault) 3600 nicolas+rss@perriault.net (Nicolas Perriault) A short introduction to Elm https://nicolas.perriault.net/talks/2017/a-short-introduction-to-elm/ <p>A short presentation about <a href="http://elm-lang.org/">Elm</a> I gave at <a href="https://www.meetup.com/fr-FR/MontpellierJS/">MontpellierJS</a>.</p> <figure> <img src="/static/talks/2017/elm-presentation.jpg" alt=""> <figcaption> Image courtesy of <a href="http://www.weeple.fr/">Fabrice Bentz</a></figcaption> </figure> <iframe src="//slides.com/n1k0/elm/embed" width="660" height="420" scrolling="no" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen> <p>Your browser doesn't support iframes it seems.</p> </iframe> Wed, 25 Jan 2017 00:00:00 +0000 https://nicolas.perriault.net/talks/2017/a-short-introduction-to-elm/ nicolas+rss@perriault.net (Nicolas Perriault) From OSX to Ubuntu https://nicolas.perriault.net/code/2016/from-osx-to-ubuntu/ <p><strong>A year earlier I decided to switch from OSX to <a href="http://ubuntu.com">Ubuntu</a>, so now is a good time to make a little retrospective. TL;DR: Linux now offers a pleasant desktop user experience and there's no way back for me.</strong></p> <p><img alt="&quot;Thou Shall Migrate&quot;, says a funny penguin. Credits: Hamish Irvine" src="/static/code/2016/thou-shall-migrate.jpg" /></p> <p>I was a Linux user 10 years ago but moved to being a Mac one, mainly because I was tired of maintaining an often broken system (hello <em>xorg.conf</em>), and Apple had quite an appealing offer at the time: a well-maintained Unix platform matching beautiful hardware, sought-after UX, access to editor apps like Photoshop and MS Office, so best of both worlds.</p> <p>To be frank, I was a happy Apple user in the early years, then the shine started to fade; messing up your system after upgrades became more frequent, Apple apps grown more and more bloated and intrusive (hello iTunes), UX started turning Kafkaian at times, too often I was finding myself tweaking and repairing stuff from the terminal...</p> <p>The trigger was pulled when Apple announced their 2015 MacBook line, with strange connectivity decisions like having a unique port for everything and using dongles: <a href="https://www.wired.com/2015/03/life-macbooks-single-port-wont-easyyet/">meh</a>. If even their top notch hardware started to turn weird, it was probably time to look elsewhere. And now I see their latest MBP line with the <a href="http://mashable.com/2016/10/27/twitter-reactions-apple-removes-escape-key/">Esc key removed</a> (so you can't escape anymore, haha), I'm kinda comforted in my decision.</p> <p>Meanwhile, since I've joined Mozilla and the <a href="https://servicedenuages.fr/">Storage team</a>, I could see many colleagues happily using Linux, and it didn't feel like they were struggling with anything particular. Oddly enough, it seemed they were capable of working efficiently, both for professional and personal stuff.</p> <p>I finally took the plunge and ordered a <a href="http://shop.lenovo.com/us/en/laptops/thinkpad/x-series/x1-carbon/">Lenovo X1 Carbon</a>, then started my journey to being a Linux user again.</p> <h2>Choosing a distro</h2> <p>I didn't debate this for days, I installed the latest available <a href="http://ubuntu.com">Ubuntu</a> right away as it was the distribution I was using before moving to OSX (I even <a href="http://www.eyrolles.com/Informatique/Livre/ubuntu-9782212116083">contributed</a> to a book on it!). I was used to Debian-based systems and knew Ubuntu was still acclaimed for its ease of use and great hardware support. I wasn't disappointed as on the X1 everything was recognized and operational right after the installation, including wifi, bluetooth and external display.</p> <p>I was greeted with the <a href="https://unity.ubuntu.com/">Unity</a> desktop, which was disturbing as I was a Gnome user back in the days. Up to a point I installed the latter, though in its <a href="https://www.gnome.org/gnome-3/">version 3 flavor</a>, which was also new to me.</p> <p>I like Gnome3. It's simple, configurable and made me feel productive fast. Though out of bad luck or skills and time to spend investigating, a few things were not working properly: fonts were huge in some apps and normal in others, external display couldn't be configured to a different resolution and dpi ratio than my laptop's, things like that. After a few weeks, I switched back to Unity, and I'm still happily using it today as it has nicely solved all the issues I had with Gnome (which I still like a lot though).</p> <h2>The pain points when coming from OSX</h2> <p>Let's be honest, the Apple keyboard French layout is utter crap, but as many things involving muscle memory, once you're used to it, it's a pain in the ass to readapt to anything else. I struggled for something like three weeks fighting old habits in this area, then eventually got through.</p> <p>Last, a bunch of OSX apps are not available on Linux, so you have to find their equivalent, when they exist. The good news is, most often they do.</p> <h2>The Web is your App Store</h2> <p>What also changed in last ten years is the explosion of the Web as an application platform. While <a href="https://www.libreoffice.org/">LibreOffice</a> and <a href="https://www.gimp.org/">The Gimp</a> are decent alternatives to MS Office and Photoshop, you now have access to many similarly scoped Web apps like Google Docs and <a href="https://pixlr.com/">Pixlr</a>, provided you're connected to the Internet. Just ensure using a modern Web browser like Firefox, which luckily ships by default in Ubuntu.</p> <p>For example I use <a href="https://www.irccloud.com/">IRCCloud</a> for IRC, as Mozilla has a corporate account there. The cool thing is it acts as a bouncer so it keeps track of messages when you go offline, and has a nice <a href="https://play.google.com/store/apps/details?id=com.irccloud.android.enterprise&amp;hl=en">Android app</a> which syncs.</p> <h2>When the Web isn't enough</h2> <p>There is obviously lots of things Web apps can't do, like searching your local files or updating your system. And let's admit that sometimes for specific tasks native apps are still more efficient and better integrated (by definition) than what the Web has to offer.</p> <h3>Launcher &amp; file search</h3> <p>I was a hardcore <a href="https://www.alfredapp.com/">Alfred.app</a> user on OSX. On Linux there's quite no strict equivalent though <a href="https://help.ubuntu.com/lts/ubuntu-help/unity-dash-intro.html">Unity Dash</a>, <a href="https://github.com/ManuelSchneid3r/albert">Albert</a> or <a href="http://lifehacker.com/5704221/synapse-is-a-super-fast-tightly-integrated-application-launcher-for-linux">synapse</a> can cover most of its coolness.</p> <figure> <img src="/static/code/2016/unity-dash.gif" alt="Unity Dash in action" style="width:100%;border-radius:5px"> <figcaption>Unity Dash in action</figcaption> </figure> <figure> <img src="/static/code/2016/synapse.gif" alt="synapse in action" style="width:100%;border-radius:5px"> <figcaption>synapse in action</figcaption> </figure> <p>If you use the text shortcuts feature of Alfred (or if you use <a href="https://smilesoftware.com/textexpander">TextExpander</a>), you might be interested in <a href="https://github.com/autokey/autokey/wiki">AutoKey</a> as well.</p> <h3>File manager</h3> <p>I couldn't spot any obvious usability difference between <a href="https://wiki.gnome.org/action/show/Apps/Nautilus">Nautilus</a> and the <a href="https://support.apple.com/en-us/HT201732">OSX Finder</a>, but I mostly use their basic features anyway.</p> <p><img alt="Nautilus in action" src="/static/code/2016/nautilus.png" /></p> <p>To emulate Finder's <a href="https://en.wikipedia.org/wiki/Quick_Look">QuickLook</a>, <a href="https://community.linuxmint.com/software/view/gnome-sushi">sushi</a> does a proper job.</p> <h3>Code editors</h3> <p>The switch shouldn't be too hard as most popular editors are available on Linux: <a href="https://www.sublimetext.com/3">Sublime Text</a>, <a href="https://atom.io/">Atom</a>, <a href="https://code.visualstudio.com/download">VSCode</a> and obviously vim and emacs.</p> <p><img alt="" src="/static/code/2016/atom.png" /></p> <h3>Terminal</h3> <p>I was using <a href="https://www.iterm2.com/">iTerm2</a> on OSX, so I was happy to find out about <a href="https://gnometerminator.blogspot.fr/p/introduction.html">Terminator</a>, which also supports tiling &amp; split panes.</p> <h3>Task switching, exposé</h3> <p>Unity provides a classic <code>alt+tab</code> switcher and an Exposé-style overview, just like OSX.</p> <p><img alt="Exposé in Unity" src="/static/code/2016/expose.jpg" /></p> <h3>Photography</h3> <p>I've been a super hardcore <a href="https://lightroom.adobe.com/">Lightroom</a> user and lover, but eventually found <a href="http://www.darktable.org/">Darktable</a> and am perfectly happy with it now. Its ergonomics take a little while to get used to though.</p> <p><img alt="DarkTable in action" src="/static/code/2016/darktable.png" /></p> <p>If you want to get an idea of what kind of results it can produce, take a look at my <a href="https://500px.com/n1k0/galleries/nyc">NYC gallery on 500px</a>, fwiw all the pictures have been processed using DarkTable.</p> <p><a href="https://500px.com/n1k0/galleries/nyc"><img alt="Sample picture processed with DarkTable" src="/static/code/2016/darktable-sample.jpg" /></a></p> <p><small><em>Disclaimer: if you find these pictures boring or ugly, it's probably me and not DarkTable.</em></small></p> <p>For things like cropping &amp; scaling images, <a href="https://www.gimp.org/">The Gimp</a> does an okay job.</p> <p>For organizing &amp; managing a gallery, <a href="https://wiki.gnome.org/Apps/Shotwell">ShotWell</a> seems to be what many people use nowadays, though I'm personally happy just using my file manager somehow.</p> <h3>Games</h3> <p>Ah the good old days when you only had <a href="https://wiki.gnome.org/Apps/Aisleriot">Gnome Solitaire</a> to have a little fun on Linux. Nowadays even <a href="http://store.steampowered.com/linux">Steam</a> is available for Linux, with more and more titles available. That should get you covered for a little while.</p> <p>If it doesn't, <a href="https://www.playonlinux.com/">PlayOnLinux</a> allows running Windows games on <a href="https://www.winehq.org/">Wine</a>. Most of the time, it works just fine.</p> <p><img alt="Battle.net via PlayOnLinux" src="/static/code/2016/playonlinux-battlenet.jpg" /></p> <h3>Music &amp; Sound</h3> <p>I've been a <a href="https://www.spotify.com/fr/download/linux/">Spotify</a> user &amp; customer for years, and am very happy with the Linux version of its client.</p> <p><img alt="The Spotify Linux client" src="/static/code/2016/spotify-linux.jpg" /></p> <p>I'm using a <a href="https://www.bose.com/en_us/products/speakers/wireless_speakers/soundlink_mini_ii.html">Bose Mini SoundLink</a> over bluetooth and never had any issues pairing and using it. To be 100% honest, <em>PulseAudio</em> crashed a few times but the system has most often been able to recover and enable sound again without any specific intervention from me.</p> <p>Byt the way, it's not always easy to switch between audio sources; <a href="https://yktoo.com/en/software/indicator-sound-switcher">Sound Switcher Indicator</a> really helps by adding a dedicated menu in the top bar:</p> <p><img alt="The Sound Switcher Indicator in action" src="/static/code/2016/sound-switcher-indicator.png" /></p> <h3>Video editing</h3> <p>I'm definitely not an expert in the field but have sometimes needs for quickly crafting short movies for friends and family. <a href="https://kdenlive.org/features/">kdenlive</a> has just done its job perfectly so far for me.</p> <h3>Password manager</h3> <p>While studying password managers for work lately, I've stumbled upon <a href="https://www.enpass.io/">Enpass</a>, it's a good equivalent of <a href="https://1password.com/">1Password</a> which doesn't have a Linux version of their app. Enpass has extensions for the most common browsers, and can sync to <a href="https://dropbox.com/">Dropbox</a> or <a href="https://owncloud.org/">Owncloud</a> among other cloud services.</p> <p><img alt="Enpass in action" src="/static/code/2016/enpass.png" /></p> <h3>Cloud backup &amp; syncing</h3> <p>I was using <a href="https://dropbox.com/">Dropbox</a> and <a href="https://www.crashplan.com/">CrashPlan</a> on OSX, guess what? I'm using them on Linux too.</p> <h2>A few other niceties</h2> <h3>ScreenCloud</h3> <p><a href="https://screencloud.net/">ScreenCloud</a> allows making screenshots, annotate them and export them to different targets like the filesystem or online image hosting providers like <a href="http://imgur.com/">imgur</a> or <a href="https://dropbox.com/">DropBox</a>.</p> <p><img alt="ScreenCloud" src="/static/code/2016/screencloud-capture.png" /></p> <h3>Clipboard manager</h3> <p><a href="https://wiki.ubuntu.com/Diodon">Diodon</a> is a simple yet efficient clipboard manager, exposing a convenient menu in the system top bar.</p> <h3>RedShift</h3> <p>If you know <a href="https://justgetflux.com/">f.lux</a>, <a href="http://jonls.dk/redshift/">RedShift</a> is an alternative to it for Linux. The program will adapt the tint of your displays to the amount of light at this specific time of the day. Recommended.</p> <h3>Caffeine</h3> <p><a href="https://launchpad.net/caffeine">Caffeine</a> is a status bar application able to temporarily prevent the activation of both the screensaver and the <em>sleep</em> powersaving mode. Most useful when watching movies.</p> <h2>So, is <a href="http://www.islinuxreadyforthedesktop.com/">Linux ready for the desktop?</a></h2> <p>For <em>me</em>, the answer is yes.</p> <h2>Updates</h2> <p>I've been asked several questions by email, IRC, twitter and in the <a href="https://news.ycombinator.com/item?id=13361019">HN thread</a> about this post, here are some answers in a random order.</p> <h3>What is the exact model of your laptop?</h3> <p>Lenovo X1 Carbon 3rd Gen.</p> <h3>Do you have issues with acpi/sleep?</h3> <p>No.</p> <h3>How's battery life?</h3> <p>Obviously worse than a MacBook (where controlled hardware &amp; drivers are heavily optimized for that purpose), but not that bad tbh. I can work for max 5 hours straight, though if I start compiling stuff (hello gecko) it gets <em>really</em> bad.</p> <h3>Does the fingerprint reader work out of the box?</h3> <p>No, I tried to use <a href="https://launchpad.net/~fingerprint/+archive/ubuntu/fingerprint-gui">Fingerprint-GUI</a> but it was so unstable that I removed it. I'm easy typing passphrases anyway.</p> <h3>Did you try Krita? It's a mix between Photoshop and Paint</h3> <p>That sounds rather ambitious, and I didn't feel like installing all these KDE/Qt packages for trying it out. From the captures I could find online, it looks like a great option though.</p> <h3>There's a Linux version of <a href="https://justgetflux.com/">f.lux</a>!</h3> <p>Yeah. Also I've learned that f.lux was inspired by Redshift and not the other way around. Point taken, thanks.</p> <h3>DarkTable doesn't do X, Y and Z while Lightroom does</h3> <p>DarkTable is free. Also, its keystones-based perspective correction module is much better than anything I could find for LightRoom.</p> <p>But yeah, overall LightRoom is way ahead, and if Adobe was kind enough to port it to Linux I'd buy and use it in a heartbeat.</p> <h3>DarkTable can crop and scale images too</h3> <p>Do you often fire DarkTable to edit a screenshot?</p> <h3>Arch is so much better</h3> <p>Good for you! Diversity is nice.</p> <h3>You said you contributed to a book on Ubuntu, you're biased towards Apple</h3> <p>Haha, nice try.</p> <h3>What GTK/unity theme are you using?</h3> <p>I'm using <a href="http://www.ravefinity.com/p/vivacious-colors-gtk-theme.html">Vivacious Dark</a> in its <em>graphite</em> variant.</p> <h3>What side launcher are you using in the screenshots?</h3> <p>It's the standard Unity one with the <a href="http://askubuntu.com/a/85366/470966">icon borders removed</a>.</p> Sun, 08 Jan 2017 00:00:00 +0000 https://nicolas.perriault.net/code/2016/from-osx-to-ubuntu/ nicolas+rss@perriault.net (Nicolas Perriault) Kinto, une alternative libre à Parse et Firebase https://nicolas.perriault.net/talks/2016/kinto-une-alternative-libre-a-parse-et-firebase/ <p><em>Conférence donnée au <a href="https://2016.capitoledulibre.org/">Capitole du Libre</a> le 29 novembre 2016.</em></p> <p><strong>À l'heure où beaucoup s'interrogent sur le contrôle que l'on a sur nos données applicatives, le projet <a href="https://www.kinto-storage.org/">Kinto</a> tente d'apporter sa vision d'une solution générique à la problématique.</strong></p> <p>Il était une fois votre énième idée de projet d'application révolutionnaire uberisant la disruption personnelle, avec les sempiternelles questions qui vont avec:</p> <ul> <li>Où stocker les données ?</li> <li>Comment les maintenir de façon sécurisée ?</li> <li>Comment les synchroniser, les répliquer ?</li> </ul> <p>De nombreux services existent dans le cloud, peu répondent favorablement à l'ensemble de ces interrogations. Kinto est une base de données JSON auto-hébergeable disposant d'une API REST simple à utiliser, permettant d'administrer et synchroniser les données de façon sécurisée.</p> <video width="660" height="420" controls="controls"> <source type="video/mp4" src="http://videos2016.capitoledulibre.org/technologies-web/n1k0-kinto-une-alternative-libre-a-parse-et-firebase.mp4"/> </video> <iframe src="//slides.com/n1k0/kinto-capitole/embed" width="660" height="420" scrolling="no" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen> <p>Your browser doesn't support iframes it seems.</p> </iframe> Tue, 29 Nov 2016 00:00:00 +0000 https://nicolas.perriault.net/talks/2016/kinto-une-alternative-libre-a-parse-et-firebase/ nicolas+rss@perriault.net (Nicolas Perriault) To the Light https://nicolas.perriault.net/photography/2014/to-the-light/ <p><img src="/static/photography/2014/to-the-light/main.jpg" alt="To the Light by Nicolas Perriault"></p><p>Palavas, France</p><p>Palavas, France. Taken with the Nexus5. This damn little thing happens to be a great camera so far.</p> Wed, 01 Jan 2014 00:00:00 +0000 https://nicolas.perriault.net/photography/2014/to-the-light/ nicolas+rss@perriault.net (Nicolas Perriault) Résiliation, piège à cons https://nicolas.perriault.net/carnet/2013/resiliation-piege-a-cons/ <p>Ça fait plusieurs fois que j’ai cette conversation, <em>quelles sont les limites acceptables pour obtenir la fidélité d’un utilisateur ?</em></p> <p>Prenons l’exemple de l’abonnement <a href="http://www.mediapart.fr/">Mediapart</a>.</p> <blockquote class="twitter-tweet tw-align-center" lang="fr"><p>WOW. Donc il faut envoyer un courrier *papier* à Mediapart — le journal *en ligne* — pour résilier son abonnement. Bravo.</p>&mdash; Nicolas Perriault (@n1k0) <a href="https://twitter.com/n1k0/statuses/253783723546382336">October 4, 2012</a></blockquote> <p>Autant vous pouvez facilement souscrire un abonnement via un simple formulaire en ligne, autant pour vous désabonner, boum, <a href="http://bricablog.net/dotclear/index.php/post/2013/12/26/Pourquoi-je-ne-lis-plus-Mediapart-ou-la-r%C3%A9ciprocit%C3%A9-des-principes%2C-fussent-ils-de-valeurs-in%C3%A9gales">courrier postal obligatoire</a> (ça me rassure de pas être le seul aigri dans l’histoire). Pas de résiliation en ligne donc, sur un journal <em>en ligne</em>. Évidemment, cette démarche freine pas mal de procrastinateurs, qui se consolent généralement en contribuant au financement d’un organe de presse indépendant pour 9 euros par mois (sinon, la Croix Rouge c’est pas mal non plus hein).</p> <p>D’aucuns comprendront que la stratégie mise en œuvre ici est de profiter de la fainéantise de nombreuses personnes quant aux démarches administratives même les plus simples <small>(encore qu’associer la Poste et simplicité font d’emblée emmerger quelques doutes légitimes)</small>.</p> <blockquote class="twitter-tweet tw-align-center" lang="fr"><p><a href="https://twitter.com/n1k0">@n1k0</a> je ne vais pas rentrer dans ce débat là :) on peut demander à d&#39;autres poirquoi un RAR aussi (cc <a href="https://twitter.com/arnaudlimbourg">@arnaudlimbourg</a> )</p>&mdash; Nicolas Silberman (@nsilberman) <a href="https://twitter.com/nsilberman/statuses/253788177632985088">October 4, 2012</a></blockquote> <p style="text-align:center"><small>(ah bon d’accord.)</small></p> <p>La dernière fois que j’ai eu affaire à ce type de procédé, c’est avec Canal +. Ces gens-là utilisent la même technique en la poussant de façon plus extrême encore : si vous disposez d’une Freebox, vous pouvez activer un abonnement Canal + depuis l’interface de la box, et hop, accès à Canal + directement opérationnel, waouh. Par contre, pour résilier… Lettre recommandé avec A/R a minima deux mois avant la date anniversaire de la souscription (vous avez bien lu). Encore une fois, les procrastinateurs, rétifs au stratif et handicapés du calendrier de tous bords en seront pour leur frais.</p> <p>Souvent, lorsque j’évoque ces deux exemples, certaines personnes — parmi lequelles des gens bien sous tous rapports — me rétorquent que <a href="https://twitter.com/search?q=%23lesgens">#lesgens</a> n’ont qu’à pas procrastiner, à être organisés, que finalement c’est plutôt <em>bien fait pour eux</em>. Que ça <em>leur apprendra</em>. Qu’ils avaient qu’à pas être aussi faibles, qu’à pas être aussi cons, quoi. <em>Darwin for the win.</em></p> <p>D’autres me disent que <em>tant que c’est légal</em>, ils n’y voient aucun souci, voire même un business model plutôt <a href="http://scopyleft.fr/honnetete/">malin</a>, Tintin. Les victimes n’avaient qu’à faire attention, à bien lire les CGV de 180 pages, tout ça. La faiblesse comme fond de commerce, c’est pas trop la classe ?</p> <p>Et ça me fait quand même un tout petit peu de peine. Ces interlocuteurs me disent en gros que profiter d’une faiblesse, même infime, de l’utilisateur est une démarche parfaitement normale, particulièrement lorsqu’elle peut garantir des rentrées d’argent confortables et régulières à peu de frais. Voire même de faire des économies substantielles sur les coûts d’infrastructure, vu que ces gens-là n’utiliseront vraisemblablement peu ou plus les services en question. Voire enfin que peu importent les moyens mis en œuvre pour financer une cause que l’on estime soi-même juste.</p> <p>Bon, vous pensez que j’exagère, là hein ? Que j’en rajoute, qu’il n’y a pas autant de "victimes" de ce type de procédés que ça, que c’est anecdotique voire psychiatrique ?</p> <p>Prenez 10 minutes pour faire un petit exercice rigolo : épluchez vos derniers relevés bancaires et pointez systématiquement les dépenses de type prélèvement automatique. Vous pourriez vous surprendre vous-même.</p> <p>C’est ce qui m’est arrivé dernièrement en tout cas, dans une démarche d’abaissement de mon empreinte économique pour arriver à vivre décemment avec moins d’argent, de façon plus raisonnable et raisonnée. C’est fou ce qu’on peut souscrire comme conneries, d’un simple clic… et oublier.</p> <p>Mais revenons-en à nos moutons ; que se passe t-il dans le cas de Mediapart ou de Canal + quand je découvre les valeurs impliquées derrières le procédé de résiliation ? Peu importe si je me désabonne quand même, c’est peut-être le moins pire des effets collatéraux dans l’histoire… Ce qui se passe de grave pour ces entités est <strong>que la confiance est rompue</strong>. Dans le cas de Canal +, j’avoues qu’elle n’a jamais été bien haute de toute façon, mais dans le cas Mediapart, quelle claque. Et j’en parle. On parle bien plus volontiers des claques que l’on prend autour de soi.</p> <p>Par pitié, si vous gérez un business, quel qu’il soit, essayez de réaliser, de comprendre que <strong>prendre les utilisateurs pour des cons n’est pas une stratégie de développement durable sur le long terme.</strong> Respectez-les autant que vous vous respectez. Et si vous ne vous respectez pas, foutez-leur la paix.</p> <p id="maj"><strong>Mise à jour :</strong></p> <p><a href="http://deboisset.fr/">Hadrien de Boisset</a> me signale par email :</p> <blockquote> <p>En droit français, la signature d’un contrat papier n’est pas nécessaire pour que ce contrat existe (pour des contrats de moins de 1500€). Une fois le contrat conclu, il s’impose aux parties au même titre que la loi. La résiliation, qui met fin unilatéralement à un contrat, doit en revanche toujours être demandée sur papier, quelle que soit la valeur du contrat, c’est une obligation légale qui vise à protéger les fournisseurs contre d’éventuels procès futurs pour non respect du contrat en leur faisant parvenir une preuve de la volonté du client d’y mettre fin.</p> </blockquote> <p><strong>Ma réaction :</strong></p> <p>J'admets bien volontiers ne pas m'être renseigné sur le cadre légal entourant la contractualisation commerciale en droit français ; merci pour ces précisions ! Malgré tout, ces dispositions me surprennent énormément du fait de l’absence de symétrie entre souscription et résiliation, et je ne peux m’empêcher d’y voir une forme de légitimation institutionnelle à faciliter l’acte d’engagement versus la cessation dudit engagement. Car il me semble tout aussi délicat pour un fournisseur d’être attaqué sur un processus de souscription abusif ou frauduleux que sur celui de la résiliation.</p> <p>Pourrait-on y voir une forme de <em>facilitation</em> à la commercialisation, voire d’une “difficultation” du processus inverse ? Je comprends que les chiffres de la croissance tiennent énormément à cœur à nos politiques, mais tout de même !<code>&lt;/complot&gt;</code></p> <p>En tout état de cause, une stricte symétrie des moyens minimum autorisés pour souscrire ou résilier un contrat me semblerait personnellement aller de soi. Même si je me doute que — comme souvent j’ai l’impression — certains abus ont fait aboutir au cadre légal actuel…</p> Mon, 30 Dec 2013 00:00:00 +0000 https://nicolas.perriault.net/carnet/2013/resiliation-piege-a-cons/ nicolas+rss@perriault.net (Nicolas Perriault) Functional JavaScript for crawling the Web https://nicolas.perriault.net/code/2013/functional-javascript-for-crawling-the-web/ <p>I've been giving JavaScript &amp; <a href="http://casperjs.org/">CasperJS</a> training sessions lately, and was amazed how few people are aware of the <a href="http://en.wikipedia.org/wiki/Functional_Programming">Functional Programming</a> capabilities of JavaScript. Many couldn't see obvious usage of these in Web development, which is a bit of a shame if you ask me.</p> <p>Let's take things like <code>map</code> and <code>reduce</code> from the <code>Array</code> prototype:</p> <pre><code>function square(x) { return x * x; } function sum(x, y) { return x + y; } [1, 2, 3].map(square).reduce(sum) // 14 </code></pre> <p>I've been hearing a few times things like:</p> <blockquote> <p>Well yeah that's cool, but I don't do maths, I'm a Web developer.</p> </blockquote> <p>And each time it turns me a little sad.</p> <h3>Disclaimer</h3> <p>As we're programming language hipsters, in this article we'll use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/arrow_functions">ES6 short function syntax</a> which has landed a few weeks ago in <a href="http://nightly.mozilla.org/">Firefox Nightlies</a> and eases a lot writing code in the functional style:</p> <pre><code>var square = x =&gt; x * x; var sum = (x, y) =&gt; x + y; [1, 2, 3].map(square).reduce(sum) // 14 </code></pre> <p>We'll also use other ES6 features as well because, you know, today is our future already.</p> <p>This article contents will also probably hurt some people feelings, probably because there's a lot to hate in there when you come from a pure OOP landscape. <strong>Please think of this article as an exercise of thought instead of yet another new JavaScript tutorial™.</strong></p> <h3>Crawling the DOM using FP</h3> <p>Take this DOM fragment featuring a good ol' data table as an example:</p> <pre><code>&lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Country&lt;/th&gt; &lt;th&gt;Population (M)&lt;/th&gt; &lt;th&gt;GNP (B)&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt;&lt;td&gt;Belgium&lt;/td&gt;&lt;td&gt;11.162&lt;/td&gt;&lt;td&gt;419&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;France&lt;/td&gt;&lt;td&gt;63.820&lt;/td&gt;&lt;td&gt;2246&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Germany&lt;/td&gt;&lt;td&gt;80.640&lt;/td&gt;&lt;td&gt;3139&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Greece&lt;/td&gt;&lt;td&gt;10.758&lt;/td&gt;&lt;td&gt;298&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Italy&lt;/td&gt;&lt;td&gt;59.789&lt;/td&gt;&lt;td&gt;1871&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Netherlands&lt;/td&gt;&lt;td&gt;16.795&lt;/td&gt;&lt;td&gt;713&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Poland&lt;/td&gt;&lt;td&gt;38.548&lt;/td&gt;&lt;td&gt;782&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Portugal&lt;/td&gt;&lt;td&gt;10.609&lt;/td&gt;&lt;td&gt;252&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;United Kingdom&lt;/td&gt;&lt;td&gt;64.231&lt;/td&gt;&lt;td&gt;2290&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt;&lt;td&gt;Spain&lt;/td&gt;&lt;td&gt;46.958&lt;/td&gt;&lt;td&gt;1432&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; </code></pre> <p>To map the country names to a regular array of strings:</p> <pre><code>var rows = document.querySelectorAll("tbody tr"); [].map.call(rows, row =&gt; row.querySelector("td").textContent); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>It worked: our map operation transformed a list of DOM table row elements to the text value of their very first cell. Well, it feels like we could probably enhance the code ergonomics a bit here.</p> <blockquote> <p><strong>Note:</strong> <em>If you wonder why do we use <code>[].map.call</code> in lieu of just calling <code>map</code> from the element list prototype, that's because <code>NodeList</code> doesn't implement the <code>Array</code> interface… <small>Yeah, I know.</small></em></p> </blockquote> <p>As an illustrative exercise, let's write our own <code>map</code> function to make a passed iterable always exposing the <code>Array</code> interface; also, let's invert the order of passed args to ease further composability (more on this later):</p> <pre><code>const map = (fn, iterable) =&gt; [].map.call(iterable, fn); </code></pre> <blockquote> <p><strong>Note:</strong> we declare <code>map</code> as a constant to avoid any accidental mess. Also, I don't see obvious reasons for a function to be mutated here.</p> </blockquote> <p>So we can write:</p> <pre><code>var rows = document.querySelectorAll("tbody tr"); map(row =&gt; row.querySelector("td").textContent, rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>As a side note, this <code>map</code> implementation also works for strings:</p> <pre><code>map(x =&gt; x.toUpperCase(), "foo"); // ["F", "O", "O"] </code></pre> <p>We can also write a tiny abstraction on top of <code>querySelectorAll</code>, again to ensure further composability:</p> <pre><code>const nodes = (sel, root) =&gt; (root || document).querySelectorAll(sel); </code></pre> <p>So now we can write:</p> <pre><code>var rows = nodes("tbody tr"); map(node =&gt; nodes("td", node)[0].textContent, rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>Hmm, the operations being performed within the function passed to <code>map</code> (finding a first child node, getting an element property value) sound like things we're most likely to do many times while extracting information from the DOM. And then we'd probably want better code semantics as well.</p> <p>For starters, let's create a <code>first()</code> function for finding the first element out of a collection:</p> <pre><code>const first = iterable =&gt; iterable[0]; // first([1, 2, 3]) =&gt; 1 </code></pre> <p>Our example becomes:</p> <pre><code>map(node =&gt; first(nodes("td", node)).textContent, rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>In the same vein, we could use a <code>prop()</code> <a href="http://en.wikipedia.org/wiki/Higher-order_function">higher order function</a> — basically a function returning a function — one more time to create a reusable &amp; composable property getter (we'll get back to this, read on):</p> <pre><code>const prop = name =&gt; object =&gt; object[name]; // const getFoo = prop("foo"); // getFoo({foo: "bar"}) =&gt; "bar" </code></pre> <p>If you struggle understanding how this works, this is how we would write <code>prop</code> using current function syntax:</p> <pre><code>function prop(name) { return function(object) { return object[name]; }; } </code></pre> <p>Let's use our new property getter generator:</p> <pre><code>const getText = prop("textContent"); map(node =&gt; getText(first(nodes("td", node))), rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>Now, how about having a generic for finding a node's child elements from a selector? Let's do this:</p> <pre><code>const finder = selector =&gt; root =&gt; nodes(selector, root); const findCells = finder("td"); findCells(document.querySelector("table")).length // 30 </code></pre> <p>Don't panic, again this is how we'd write it using standard function declaration syntax:</p> <pre><code>function finder(selector) { return function(root) { return nodes(selector, root); } } </code></pre> <p>Let's use it:</p> <pre><code>const getText = prop("textContent"); const findCells = finder("td"); map(node =&gt; getText(first(findCells(node))), rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>At this point, you may be wondering how this is possibly improving code readability and maintainability… Now is the perfect time to use <a href="http://en.wikipedia.org/wiki/Function_composition">function composition</a> (you waited for it), to aggregate &amp; chain minimal bits of reusable code.</p> <blockquote> <p>Note: If you're familiar with the UNIX philosophy, that's exactly the same approach as when using the pipe operator:</p> <pre><code> $ ls -la | awk '{print $2}' | grep pattern | wc -l </code></pre> </blockquote> <p>Let's create a <code>sequence</code> function to help composing functions sequentially:</p> <pre><code>const sequence = function() { return [].reduce.call(arguments, function(comp, fn) { return () =&gt; comp(fn.apply(null, arguments)); }); }; </code></pre> <p>This one is a bit complicated; it basically takes all functions passed as arguments and returns a new function capable of processing them sequencially, passing to each the result of the previous execution:</p> <pre><code>const squarePlus2 = sequence(x =&gt; 2 + x, x =&gt; x * x); squarePlus2(4); // 4 * 4 + 2 =&gt; 18 =&gt; Aspirine is in the bathroom </code></pre> <p>In classic notation without using a sequence, that would be the equivalent of:</p> <pre><code>function plus2(x) { return 2 + x; } function square(x) { return x * x; } function squarePlus2(x) { return plus2(square(x)); } squarePlus2(4); // 18 </code></pre> <p>By the way, <code>sequence</code> is a very good place to use <a href="https://blog.mozilla.org/jorendorff/2012/05/29/rest-arguments-and-default-arguments-in-javascript/">ES6 Rest Arguments</a> which have also landed recently in Gecko; let's rewrite it accordingly:</p> <pre><code>const sequence = function(...fns) { return fns.reduce(function(comp, fn) { return (...args) =&gt; comp(fn.apply(null, args)); }); }; </code></pre> <p>Let's use it in our little DOM crawling example:</p> <pre><code>const getText = prop("textContent"); const findCells = finder("td"); map(sequence(getText, first, findCells), rows) // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>What I like the most about the FP style is that it actually describes fairly well what's going to happen; you can almost read the code as you'd read plain English <em>(caveat: don't do this at family dinners).</em></p> <p>Also you may want to have the functions passed in the opposite order, ala UNIX pipes, which usually enhances legibility a bit for seasonned functional programmers; let's create a <code>compose</code> function for doing just that:</p> <pre><code>const compose = (...fns) =&gt; sequence.apply(null, fns.reverse()); map(compose(findCells, first, getText), rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <h3>Wait, is this really better?</h3> <p>As a side note, one may argue that:</p> <pre><code>map(sequence(getText, first, findCells), rows); </code></pre> <p>Is not much really better than:</p> <pre><code>map(row =&gt; getText(first(findCells(row))), rows); </code></pre> <p>Though the composed approach is probably more likely to scale when adding many more functions to the stack:</p> <pre><code>a(b(c(d(e(f(g(h(foo)))))))); sequence(a, b, c, d, e, f, g, h)(foo); </code></pre> <p>Last, a composed function is itself composable by essence, and that's probably a killer feature:</p> <pre><code>map(sequence(getText, sequence(first, findCells)), rows); // ["Belgium", "France", "Germany", "Greece", "Italy", …] </code></pre> <p>Which something like this:</p> <pre><code>var crawler = new Crawler("table"); crawler.findCells("tbody tr").first().getText(); </code></pre> <p>Is hardly likely to offer.</p> <h2>A few more examples</h2> <p>To compute the total population of listed countries:</p> <pre><code>const reduce = (fn, init, iterable) =&gt; [].reduce.call(iterable, fn, init); const second = (iterable) =&gt; iterable[1]; const sum = (x, y) =&gt; x + y; var populations = map(compose(findCells, second, getText, parseFloat), rows); reduce(sum, 0, populations); // 403.31000000000006 </code></pre> <p>To generate a JSON export of the whole table data:</p> <pre><code>const partial = (fn, ...r) =&gt; (...a) =&gt; fn.apply(null, r.concat(a)) const nth = n =&gt; (iterable) =&gt; iterable[n - 1]; const third = nth(3); const getTexts = partial(map, getText); const asObject = (data) =&gt; ({ name: first(data), population: parseFloat(second(data)), gnp: parseFloat(third(data)) }); var countries = map(compose(findCells, getTexts, asObject), rows); JSON.stringify(countries); // "[{"name":"Belgium","population":11.162,"gnp":419}, … </code></pre> <p>To compute the global average <acronym title="Gross National Product">GNP</acronym> per capita for these countries:</p> <pre><code>const perCapita = c =&gt; ({name: c.name, perCapita: c.gnp / c.population}); var gnpPerCapita = map(perCapita, countries); JSON.stringify(gnpPerCapita); // "[{"name":"Belgium","perCapita":37.5380756136893}, … </code></pre> <p>To filter countries having more than <code>n€</code> of GNP per capita, sort them by descending order and export the result as JSON:</p> <pre><code>const select = (fn, iterable) =&gt; [].filter.call(iterable, fn) const sort = (fn, iterable) =&gt; [].sort.call(iterable, fn); const sortDesc = partial(sort, (a, b) =&gt; a.perCapita &gt; b.perCapita ? -1 : 1); const healthy = partial(select, c =&gt; c.perCapita &gt; 38); const healthyCountries = compose(healthy, sortDesc); JSON.stringify(healthyCountries(gnpPerCapita)); // "[{"name":"Netherlands","perCapita":42.45311104495385}, … </code></pre> <p>I could probably go on and on, but you get the picture. This post is not to claim that the FP approach is the best of all in JavaScript, but that it certainly has its advantages. Feel free to play with these concepts for a while to make your mind, eventually :)</p> <p>If you're interested in Functional JavaScript, I suggest the following resources:</p> <ul> <li><a href="http://vimeo.com/43382919">Pure, functional JavaScript</a>, an inspiring talk from Christian Johansen;</li> <li><a href="https://leanpub.com/javascript-allonge/read">JavaScript Allongé</a>, an online book which covers most of its aspects in a very comprehensive style (you should buy it);</li> <li><a href="http://stevelosh.com/blog/2013/03/list-out-of-lambda/">List Out of Lambda</a>, a blog post from Steve Losh where he reinvents lists purely using functions in JavaScript (!);</li> <li>If you're hooked with FP (yay!), have a look at <a href="http://clojure.org/">Clojure</a> and its port targetting the JavaScript platform, <a href="https://github.com/clojure/clojurescript">ClojureScript</a>.</li> </ul> <p>If you're interested in ECMAScript 6, here are some good links to read about:</p> <ul> <li><a href="http://brendaneich.github.io/Strange-Loop-2012/">The state of JavaScript</a> — Brendan Eich, Strange Loop 2012</li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/ECMAScript_6_support_in_Mozilla">ECMAScript 6 support in Mozilla</a></li> </ul> Sun, 01 Dec 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/functional-javascript-for-crawling-the-web/ nicolas+rss@perriault.net (Nicolas Perriault) Get your Frontend JavaScript Code Covered https://nicolas.perriault.net/code/2013/get-your-frontend-javascript-code-covered/ <p><strong>So finally you're <a href="/code/2013/testing-frontend-javascript-code-using-mocha-chai-and-sinon/">testing your frontend JavaScript code</a>? Great! The more you write tests, the more confident you are with your code… but how much precisely? That's where <a href="http://en.wikipedia.org/wiki/Code_coverage">code coverage</a> might help.</strong></p> <p>The idea behind code coverage is to record which parts of your code (functions, statements, conditionals and so on) have been executed by your test suite, to compute metrics out of these data and usually to provide tools for navigating and inspecting them.</p> <p>Not a lot of frontend developers I know actually test their frontend code, and I can barely imagine how many of them have ever setup code coverage… Mostly because there are not many frontend-oriented tools in this area I guess.</p> <p>Actually I've only found one which provides an adapter for <a href="http://visionmedia.github.io/mocha/">Mocha</a> and actually works…</p> <blockquote class="twitter-tweet tw-align-center"> <p> Drinking game for web devs: <br> (1) Think of a noun<br> (2) Google &quot;&lt;noun&gt;.js&quot;<br> (3) If a library with that name exists - drink </p> &mdash; Shay Friedman (@ironshay) <a href="https://twitter.com/ironshay/statuses/370525864523743232">August 22, 2013</a> </blockquote> <p><strong><a href="http://blanketjs.org/">Blanket.js</a></strong> is an <em>easy to install, easy to configure, and easy to use JavaScript code coverage library that works both in-browser and with nodejs.</em></p> <p>Its use is dead easy, adding Blanket support to your Mocha test suite is just matter of adding this simple line to your HTML test file:</p> <pre><code>&lt;script src="vendor/blanket.js" data-cover-adapter="vendor/mocha-blanket.js"&gt;&lt;/script&gt; </code></pre> <p>Source files: <a href="https://raw.github.com/alex-seville/blanket/master/dist/qunit/blanket.min.js">blanket.js</a>, <a href="https://raw.github.com/alex-seville/blanket/master/src/adapters/mocha-blanket.js">mocha-blanket.js</a></p> <p>As an example, let's reuse the silly <code>Cow</code> example we used <a href="/code/2013/testing-frontend-javascript-code-using-mocha-chai-and-sinon/">in a previous episode</a>:</p> <pre><code>// cow.js (function(exports) { "use strict"; function Cow(name) { this.name = name || "Anon cow"; } exports.Cow = Cow; Cow.prototype = { greets: function(target) { if (!target) throw new Error("missing target"); return this.name + " greets " + target; } }; })(this); </code></pre> <p>And its test suite, powered by Mocha and <a href="http://chaijs.com/">Chai</a>:</p> <pre><code>var expect = chai.expect; describe("Cow", function() { describe("constructor", function() { it("should have a default name", function() { var cow = new Cow(); expect(cow.name).to.equal("Anon cow"); }); it("should set cow's name if provided", function() { var cow = new Cow("Kate"); expect(cow.name).to.equal("Kate"); }); }); describe("#greets", function() { it("should greet passed target", function() { var greetings = (new Cow("Kate")).greets("Baby"); expect(greetings).to.equal("Kate greets Baby"); }); }); }); </code></pre> <p>Let's create the HTML test file for it, featuring Blanket and its adapter for Mocha:</p> <pre><code>&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;Test&lt;/title&gt; &lt;link rel="stylesheet" media="all" href="vendor/mocha.css"&gt; &lt;/head&gt; &lt;body&gt; &lt;div id="mocha"&gt;&lt;/div&gt; &lt;div id="messages"&gt;&lt;/div&gt; &lt;div id="fixtures"&gt;&lt;/div&gt; &lt;script src="vendor/mocha.js"&gt;&lt;/script&gt; &lt;script src="vendor/chai.js"&gt;&lt;/script&gt; &lt;script src="vendor/blanket.js" data-cover-adapter="vendor/mocha-blanket.js"&gt;&lt;/script&gt; &lt;script&gt;mocha.setup('bdd');&lt;/script&gt; &lt;script src="cow.js" data-cover&gt;&lt;/script&gt; &lt;script src="cow_test.js"&gt;&lt;/script&gt; &lt;script&gt;mocha.run();&lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p><strong>Notes</strong>:</p> <ul> <li>Notice the <code>data-cover</code> attribute we added to the script tag loading the source of our library;</li> <li>The HTML test file <em>must</em> be served over HTTP for the adapter to be loaded.</li> </ul> <p>Running the tests now gives us something like this:</p> <p><img alt="screenshot" src="/static/code/2013/blanket-coverage.png" /></p> <p>As you can see, the report at the bottom highlights that we haven't actually tested the case where an error is raised in case a target name is missing. We've been informed of that, nothing more, nothing less. We simply know we're missing a test here. Isn't this cool? I think so!</p> <p>Just remember that code coverage will only <a href="http://codebetter.com/karlseguin/2008/12/09/code-coverage-use-it-wisely/">bring you numbers</a> and raw information, not actual proofs that the whole of your <em>code logic</em> has been actually covered. If you ask me, the best inputs you can get about your code logic and implementation ever are the ones issued out of <a href="http://www.extremeprogramming.org/rules/pair.html">pair programming</a> sessions and <a href="http://alexgaynor.net/2013/sep/26/effective-code-review/">code reviews</a> — but that's another story.</p> <p><strong>So is code coverage silver bullet? No. Is it useful? Definitely. Happy testing!</strong></p> Sun, 29 Sep 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/get-your-frontend-javascript-code-covered/ nicolas+rss@perriault.net (Nicolas Perriault) Testing your frontend JavaScript code using mocha, chai, and sinon https://nicolas.perriault.net/code/2013/testing-frontend-javascript-code-using-mocha-chai-and-sinon/ <p><strong>As rich Web application complexity grows, if you want to keep your sanity, you need to unit test your frontend JavaScript code.</strong></p> <p>For the 4 past months, I've been working for <a href="http://mozilla.org/">Mozilla</a> on some big project where such testing strategy was involved. While I wish we could use <a href="http://casperjs.org/">CasperJS</a> in this perspective, Firefox wasn't supported at the time and we needed to ensure proper compatibility with its JavaScript engine. So we went with using <a href="http://visionmedia.github.io/mocha/">Mocha</a>, <a href="http://chaijs.com/">Chai</a> and <a href="http://sinonjs.org/">Sinon</a> and they have proven to be a great workflow for us so far.</p> <h3>The mocha testing framework and the chai expectation library</h3> <p><a href="http://visionmedia.github.io/mocha/">Mocha</a> is a test framework while <a href="http://chaijs.com/">Chai</a> is an expectation one. Let's say Mocha setups and describes test suites and Chai provides convenient helpers to perform all kinds of assertions against your JavaScript code.</p> <p>So let's say we have a <code>Cow</code> object we want to unit test:</p> <pre><code>// cow.js (function(exports) { "use strict"; function Cow(name) { this.name = name || "Anon cow"; } exports.Cow = Cow; Cow.prototype = { greets: function(target) { if (!target) throw new Error("missing target"); return this.name + " greets " + target; } }; })(this); </code></pre> <p>Nothing fancy, but we want to unit test this one.</p> <p>Both Mocha and Chai can be used in a <a href="http://nodejs.org/">Node</a> environment as well as within the browser; in the latter case, you'll have to setup a test HTML page and use special builds of those libraries:</p> <ul> <li>for Mocha: <a href="http://visionmedia.github.io/mocha/#browser-support">setup instructions</a>, <a href="https://github.com/visionmedia/mocha/raw/master/mocha.css">mocha.css</a>, <a href="https://github.com/visionmedia/mocha/raw/master/mocha.js">mocha.js</a></li> <li>for Chai: <a href="http://chaijs.com/guide/installation/">setup instructions</a>, <a href="http://chaijs.com/chai.js">chai.js</a></li> </ul> <p>My advice is to store these files in a <code>vendor</code> subfolder. Let's create a HTML file to test our lib:</p> <pre><code>&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;Cow tests&lt;/title&gt; &lt;link rel="stylesheet" media="all" href="vendor/mocha.css"&gt; &lt;/head&gt; &lt;body&gt; &lt;div id="mocha"&gt;&lt;p&gt;&lt;a href="."&gt;Index&lt;/a&gt;&lt;/p&gt;&lt;/div&gt; &lt;div id="messages"&gt;&lt;/div&gt; &lt;div id="fixtures"&gt;&lt;/div&gt; &lt;script src="vendor/mocha.js"&gt;&lt;/script&gt; &lt;script src="vendor/chai.js"&gt;&lt;/script&gt; &lt;script src="cow.js"&gt;&lt;/script&gt; &lt;script&gt;mocha.setup('bdd')&lt;/script&gt; &lt;script src="cow_test.js"&gt;&lt;/script&gt; &lt;script&gt;mocha.run();&lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>Please note we'll be using <a href="http://chaijs.com/api/bdd/">Chai's <em>BDD</em> Expect API</a>, hence the <code>mocha.setup('bdd')</code> call here.</p> <p>Now let's write a simple test suite for our <code>Cow</code> object constructor in <code>cow_test.js</code>:</p> <pre><code>var expect = chai.expect; describe("Cow", function() { describe("constructor", function() { it("should have a default name", function() { var cow = new Cow(); expect(cow.name).to.equal("Anon cow"); }); it("should set cow's name if provided", function() { var cow = new Cow("Kate"); expect(cow.name).to.equal("Kate"); }); }); describe("#greets", function() { it("should throw if no target is passed in", function() { expect(function() { (new Cow()).greets(); }).to.throw(Error); }); it("should greet passed target", function() { var greetings = (new Cow("Kate")).greets("Baby"); expect(greetings).to.equal("Kate greets Baby"); }); }); }); </code></pre> <p>Tests should be passing, so if you open the HTML document in your browser, you should get something like:</p> <p><img alt="sample mocha+chai results screenshot" src="/static/code/2013/cow-tests-ok.png" /></p> <p>If any of these expectations fails, you'll be notified in the test results, eg. if we change the implementation of <code>greets</code> as below:</p> <pre><code> Cow.prototype = { greets: function(target) { if (!target) throw new Error("missing target"); return this.name + " greets " + target + "!"; } }; </code></pre> <p>You'll get this instead:</p> <p><img alt="sample mocha+chai results screenshot" src="/static/code/2013/cow-tests-ko.png" /></p> <h3>How about testing asynchronous stuff?</h3> <p>Now imagine we implement a <code>Cow#lateGreets</code> method so the greetings come with a delay of one second:</p> <pre><code> Cow.prototype = { greets: function(target) { if (!target) throw new Error("missing target"); return this.name + " greets " + target + "!"; }, lateGreets: function(target, cb) { setTimeout(function(self) { try { cb(null, self.greets(target)); } catch (err) { cb(err); } }, 1000, this); } }; </code></pre> <p>We need to test this one as well, and Mocha helps us with its optional <code>done</code> callback for tests:</p> <pre><code> describe("#lateGreets", function() { it("should pass an error if no target is passed", function(done) { (new Cow()).lateGreets(null, function(err, greetings) { expect(err).to.be.an.instanceof(Error); done(); }); }); it("should greet passed target after one second", function(done) { (new Cow("Kate")).lateGreets("Baby", function(err, greetings) { expect(greetings).to.equal("Kate greets Baby"); done(); }); }); }); </code></pre> <p>Conveniently, Mocha will highlight any suspiciously long operation with red pills in case it wasn't really expected:</p> <p><img alt="sample screenshot" src="/static/code/2013/cow-async-tests-ok.png" /></p> <h3>Using Sinon for faking the environment</h3> <p>When you do unit testing, you don't want to depend on stuff external to the unit of code under test. And while avoiding your functions to have side effects is usually a good practice, in Web development it's not always easy task (think DOM, Ajax, native browser APIs, etc.)</p> <p><a href="http://sinonjs.org/">Sinon</a> is a great JavaScript library for stubbing and mocking such external dependencies and to keep control on side effects against them.</p> <p>As an example, let's imagine that our <code>Cow#greets</code> method wouldn't return a string but rather directly log them onto the browser console:</p> <pre><code>// cow.js (function(exports) { "use strict"; function Cow(name) { this.name = name || "Anon cow"; } exports.Cow = Cow; Cow.prototype = { greets: function(target) { if (!target) return console.error("missing target"); console.log(this.name + " greets " + target); } }; })(this); </code></pre> <p>How to unit test this? Well, Sinon to the rescue! First, let's add the <a href="http://sinonjs.org/releases/sinon-1.7.1.js">Sinon script</a> to our HTML test file:</p> <pre><code>&lt;!-- ... --&gt; &lt;script src="vendor/mocha.js"&gt;&lt;/script&gt; &lt;script src="vendor/chai.js"&gt;&lt;/script&gt; &lt;script src="vendor/sinon-1.7.1.js"&gt;&lt;/script&gt; </code></pre> <p>We'll <em>stub</em> the <code>console</code> object's <code>log</code> and <code>error</code> methods so we can check they're called and what's passed to them:</p> <pre><code>var expect = chai.expect; describe("Cow", function() { var sandbox; beforeEach(function() { // create a sandbox sandbox = sinon.sandbox.create(); // stub some console methods sandbox.stub(window.console, "log"); sandbox.stub(window.console, "error"); }); afterEach(function() { // restore the environment as it was before sandbox.restore(); }); // ... describe("#greets", function() { it("should log an error if no target is passed in", function() { (new Cow()).greets(); sinon.assert.notCalled(console.log); sinon.assert.calledOnce(console.error); sinon.assert.calledWithExactly(console.error, "missing target") }); it("should log greetings", function() { var greetings = (new Cow("Kate")).greets("Baby"); sinon.assert.notCalled(console.error); sinon.assert.calledOnce(console.log); sinon.assert.calledWithExactly(console.log, "Kate greets Baby") }); }); }); </code></pre> <p>Several things to be noticed here:</p> <ul> <li><code>beforeEach</code> and <code>afterEach</code> are part of the Mocha API and allow to define setup and tear down operations for each test;</li> <li>Sinon provides sandboxing, basically allowing to define and attach a set of stubs to a sandbox object you'll be able to restore at some point;</li> <li>When stubbed, <em>real</em> functions are not called at all, so here obviously nothing will be printed onto the browser console;</li> <li>Sinon ships with its own assertion library, hence the <code>sinon.assert</code> calls; a <a href="https://github.com/domenic/sinon-chai">sinon-chai</a> plugin exists for Chai, you may want to have a look at it.</li> </ul> <p><strong>There are many cool other aspects of <a href="http://visionmedia.github.io/mocha/">Mocha</a>, <a href="http://chaijs.com/">Chai</a> and <a href="http://sinonjs.org/">Sinon</a> I couldn't cover in this blog post, but I hope it opened your appetite for investigating more about them. Happy testing!</strong></p> Tue, 23 Jul 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/testing-frontend-javascript-code-using-mocha-chai-and-sinon/ nicolas+rss@perriault.net (Nicolas Perriault) CasperJS 1.1-beta1 released, with Gecko support https://nicolas.perriault.net/code/2013/casperjs-1.1-beta1-released/ <figure> <img src="/static/code/2013/fantomes.jpg" alt="" width="660"> <figcaption> © <a href="https://secure.flickr.com/photos/drooo/6946563420/">Fantôme, by DROOO</a></figcaption> </figure> <p>I'm happy to annouce the immediate availability of <a href="https://github.com/n1k0/casperjs/releases/tag/1.1-beta1">CasperJS 1.1-beta1</a>, featuring support for <a href="http://slimerjs.org/">SlimerJS</a>, which basically ports the <a href="https://github.com/ariya/phantomjs/wiki/API-Reference">PhantomJS API</a> onto the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Gecko">Gecko platform</a>.</p> <p>Yes, that means as of 1.1-beta1 you can run most of your existing CasperJS scripts against a headless Firefox (using a virtual framebuffer for now), thanks to the huge amount of effort provided by <a href="http://ljouanneau.com/">Laurent Jouanneau</a>, a long-time XUL/Gecko contributor.</p> <p>This is great news for all Web developers wanting to avoid contributing to the <a href="http://www.sitepoint.com/5-reasons-to-reject-webkit-monoculture/">establishment of a monoculture</a>.</p> <p>1.1-beta1 brings a whole lot more other features, you may want to read the <a href="https://github.com/n1k0/casperjs/releases/tag/1.1-beta1">full CHANGELOG</a>.</p> Sat, 13 Jul 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/casperjs-1.1-beta1-released/ nicolas+rss@perriault.net (Nicolas Perriault) Flatten JavaScript Pyramids with Async.js https://nicolas.perriault.net/code/2013/flatten-javascript-pyramids-with-async-js/ <figure> <img src="/static/code/2013/pyramids.png" alt=""> <figcaption> <a href="https://secure.flickr.com/photos/dkrape/5154684093/">Pyramid of Menkaure, by Darren Krape</a></figcaption> </figure> <p>I've recently open-sourced <a href="https://github.com/scopyleft/hubot-mood">hubot-mood</a>, a <a href="http://hubot.github.com/">hubot</a> script to store a team's mood and get some metrics about it. We're using it at <a href="http://scopyleft.fr/">Scopyleft</a>.</p> <p>Moods are stored in <a href="http://redis.io/">redis</a> through the <a href="https://github.com/mranney/node_redis">node-redis</a> library, which uses asynchronous calls to perform operations on the redis backend.</p> <p>So typically, to store an entry, you do something like the following:</p> <pre><code>function store(mood, cb) { redis.rpush("moods", mood, function(err) { cb(err, mood); }); } store("2013-02-01:n1k0:sunny", function(err, mood) { if (err) throw err; console.log("stored mood entry: " + mood); }); </code></pre> <p>Classic. But what if you want to perform multiple insertions, eg. to load a bunch of fixtures for your tests? I'm using <a href="http://visionmedia.github.com/mocha/">mocha</a> here:</p> <pre><code>describe("moods test", function() { // fixtures var moods = [ "2013-02-01:n1k0:sunny" , "2013-02-02:n1k0:cloudy" , "2013-02-03:n1k0:stormy" , "2013-02-04:n1k0:rainy" // … we could add many more ]; it("should do something useful with moods", function(done) { store(moods[0], function(err, mood) { assert.ifError(err); store(moods[1], function(err, mood) { assert.ifError(err); store(moods[2], function(err, mood) { assert.ifError(err); store(moods[3], function(err, mood) { assert.ifError(err); // now let's test stuff with stored moods done(); }); }); }); }); }); }); </code></pre> <p>Here we go again, <a href="http://callbackhell.com/">callback hell</a> and unmanageable pyramids.</p> <h2>Async.js to the rescue</h2> <p><a href="https://github.com/caolan/async">Async.js</a> is a node library to help dealing with <em>asynchronicity</em> and flatten pyramids. A <code>npm install async</code> later, we're ready to go:</p> <pre><code>describe("moods tests", function() { var moods = [ "2013-02-01:n1k0:sunny" , "2013-02-02:n1k0:cloudy" , "2013-02-03:n1k0:stormy" , "2013-02-04:n1k0:rainy" // … we could add many more ]; it("should do something useful with moods", function(done) { async.parallel([ function(cb) { store(mood[0], function(err, mood) { cb(err, mood); }); }, function(cb) { store(mood[1], function(err, mood) { cb(err, mood); }); }, function(cb) { store(mood[2], function(err, mood) { cb(err, mood); }); }, function(cb) { store(mood[3], function(err, mood) { cb(err, mood); }); }, ], function(err, moods) { assert.ifError(err); // now let's test stuff with stored moods done(); }); }); }); </code></pre> <h2>Wait a minute, it's not "better" at all!</h2> <p>Indeed, this is definitely not <a href="https://en.wikipedia.org/wiki/Don't_repeat_yourself">DRY code</a>. But one has to be creative to turn a tool into an efficient solution; let's invoke the powers of <a href="https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map"><code>Array#map</code></a> to build the required callback functions out of our <code>moods</code> array:</p> <pre><code>function load(fixtures, onComplete) { async.parallel(fixtures.map(function(fixture) { return function(cb) { store(fixture, function(err, result) { cb(err, result); }); }; }), onComplete); } describe("moods tests", function() { var moods = [ "2013-02-01:n1k0:sunny" , "2013-02-02:n1k0:cloudy" , "2013-02-03:n1k0:stormy" , "2013-02-04:n1k0:rainy" // … we could add many more ]; it("should do something useful with moods", function(done) { load(moods, function(err, storedMoods) { assert.ifError(err); // now let's test stuff with stored moods done(); }); }); }); </code></pre> <p><strong>Edit:</strong> there's even a built-in <code>async.map()</code> function, not sure how I missed it; so the code is even shorter:</p> <pre><code>describe("moods tests", function() { var moods = [ "2013-02-01:n1k0:sunny" , "2013-02-02:n1k0:cloudy" , "2013-02-03:n1k0:stormy" , "2013-02-04:n1k0:rainy" // … we could add many more ]; it("should do something useful with moods", function(done) { async.map(moods, store, function(err, storedMoods) { assert.ifError(err); // now let's test stuff with stored moods done(); }); }); }); </code></pre> <p><a href="https://github.com/caolan/async">Async.js</a> is a great package and one of the most popular of the <a href="http://npmjs.org/">node ecosystem</a>, but there are <a href="https://npmjs.org/browse/keyword/async">many others</a>.</p> <p>Such a library combined with a <a href="http://cjohansen.no/talks/2012/sdc-functional/">functional approach</a> provides a killer combo to solve your daily problems when programming <a href="/code/2013/why_javascript/">JavaScript</a>.</p> Tue, 05 Feb 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/flatten-javascript-pyramids-with-async-js/ nicolas+rss@perriault.net (Nicolas Perriault) Why JavaScript? https://nicolas.perriault.net/code/2013/why_javascript/ <p>I'm mostly a pythonista, though many people keep <del>trolling</del> asking me why I'm doing JavaScript, <em>oh boy, such a language that sucks</em>. I'm putting this blog post here to address this question once for all, let me hope.</p> <p>Since 15 years, I've been learning Perl, PHP, JavaScript, Python, JavaScript, Clojure, a tiny bit of Ruby, Haskell &amp; Erlang for educational purpose, and JavaScript.</p> <p>— <em>Wait a minute, JavaScript is mentioned THREE times, you mad.</em></p> <p>Yep, JavaScript — like many other programming languages probably — is one you have to learn three times to be proficient with it. The first time, to <a href="https://en.wikipedia.org/wiki/Dynamic_HTML">hate it</a>. The second time, to <a href="http://jquery.com/">start appreciating it</a>. The third time, to <a href="http://ejohn.org/apps/learn/">understand it</a> and get shit actually done.</p> <p>Now some facts:</p> <ul> <li>JavaScript surely has <a href="http://wtfjs.com/">its quirks</a> (many)</li> <li>JavaScript reminds me the cool epoch of <a href="https://en.wikipedia.org/wiki/Demoscene">demoscene</a>: example <a href="http://www.chromeexperiments.com/">1</a>, <a href="http://js1k.com/">2</a>, <a href="http://www.p01.org/releases/">3</a>, <a href="http://www.wab.com/">4</a></li> <li>JavaScript has a <a href="https://github.com/languages/JavaScript">freaking huge ecosystem</a> of tools &amp; libraries</li> <li>Some <a href="http://ejohn.org/">very</a> <a href="https://github.com/jashkenas">smart</a> <a href="http://worrydream.com/">people</a> are doing JavaScript</li> <li>JavaScript is suited for the rest of us, continuously <a href="http://worrydream.com/LearnableProgramming/">learning web hackers</a> not having a PhD in CS</li> <li>JavaScript has a <a href="http://www.codecademy.com/tracks/javascript">rather low entry barrier</a>; people can start coding and get results quickly</li> <li>JavaScript has <a href="http://www.webreference.com/programming/javascript/rg25/index.html">higher order functions</a>; that's rather important if you're interested in functional programming</li> <li>Modern JavaScript VMs are <a href="https://v8-io12.appspot.com/">fast</a></li> </ul> <p>You're free not to like JavaScript or to find it dumb. You're encouraged to let other people take the time to learn it and get shit done with it.</p> <p>So:</p> <ul> <li>Is JavaScript a usable language? Yes.</li> <li>Is JavaScript suited for all possible usages? Nope.</li> <li>Do you have to continuously rant about it? Probably not.</li> <li>Do you have to troll &amp; mock people doing JavaScript? Stop doing that now.</li> <li>Do you have to <a href="http://ejohn.org/apps/learn/">learn JavaScript</a> before criticizing it? <a href="http://nedbatchelder.com/blog/201301/stupid_languages.html">Definitely</a>.</li> </ul> <p>The problem many people have with JavaScript is they don't like it but want to join the browser party so badly — especially since even <a href="https://www.mozilla.org/en-GB/firefoxos/">cool operating systems</a> plan to use it extensively. They would just love having their Python, Ruby, Haskell, Erlang, Java, C#, COBOL, &lt;insert your favorite language here&gt; being the default standardized language to code frontend webapps in a DOM environment. Unfortunately, that's not the case; JavaScript is the default language to manipulate the DOM in browsers and make things happen. And it's not likely to change anytime soon imho. But I've got good news for you: <a href="http://altjs.org/">alternatives exist</a>.</p> <p>As an exciting side note, the next version of JavaScript, <a href="https://brendaneich.com/2012/10/harmony-of-dreams-come-true/">ECMAScript 6</a>, will definitely step up from the previous version.</p> <p>Also, a cool idea I heard would be to have a low-level compilation target platform in browsers in order to code your next website in other languages than JavaScript but with great, native performance. Having a bit what the JVM is to Java and other languages running on it. That would be smart and definitely interesting. But one more time, it's not likely to happen soon.</p> <p>In the meanwhile, JavaScript is the language of the Web, and JavaScript engines are its running platform. There are better languages, there are worse. One thing is crystal clear: great, innovative &amp; useful stuff can be achieved with it, you may want to be part of the move.</p> <p>And as always, remember that:</p> <blockquote> <p>There are only two kinds of programming languages: those people always bitch about and those nobody uses. — Bjarne Stroustrup</p> </blockquote> <figure> <a href="https://github.com/languages/"> <img src="/static/code/top-languages.png" alt=""></a> </figure> Sat, 26 Jan 2013 00:00:00 +0000 https://nicolas.perriault.net/code/2013/why_javascript/ nicolas+rss@perriault.net (Nicolas Perriault) CasperJS hits 1.0 stable https://nicolas.perriault.net/code/2012/casperjs-hits-1-0-stable/ <p><a href="http://casperjs.org/"><img src="/static/code/2012/casperjs-logo-squared-rounded.png" alt="" style="float:right;margin-left:1em;margin-bottom:.5em"></a><strong>Hey, <a href="/code/2012/introducing-casperjs-toolkit-phantomjs">long time no see</a>, right? Quite a lots of things have happened to my <em>tiny</em> <a href="http://casperjs.org/">CasperJS</a> pet project since then, which has just hit its <a href="https://github.com/n1k0/casperjs/blob/master/CHANGELOG.md#2012-12-24-v10">1.0 stable milestone</a>. Merry Christmas :)</strong></p> <p>As a reminder, <em>CasperJS is an open source navigation scripting &amp; testing utility written in Javascript and based on <a href="http://phantomjs.org/">PhantomJS</a> — the scriptable headless <a href="http://webkit.org/">WebKit</a> engine</em>.</p> <p>Now some scary metrics about the project:</p> <figure> <a href="http://gitego.com/n1k0/casperjs#watchers?interval=by_year"><img src="/static/code/2012/casperjs-github-evolution.png" alt="" style="width:100%;border-radius:5px"></a> <figcaption>Where will all of this end? Capture courtesy of <a href="http://gitego.com/n1k0/casperjs#watchers?interval=by_year">gitego.com</a></figcaption> </figure> <ul> <li>the project has now more than <a href="https://github.com/n1k0/casperjs/stargazers"><strong>1000 <em>stargazers</em></strong></a> and <a href="https://github.com/n1k0/casperjs/network"><strong>150 forks</strong></a> on github</li> <li>more than <a href="https://github.com/n1k0/casperjs/graphs/commit-activity"><strong>900 commits</strong></a> have been pushed to the master branch (430 just for the <a href="https://github.com/n1k0/casperjs/tree/gh-pages">documentation</a> one)</li> <li><a href="https://github.com/n1k0/casperjs/graphs/contributors"><strong>47 people</strong></a> have contributed patches and docs to the project; I hereby want to hug them all!</li> <li><a href="https://travis-ci.org/n1k0/casperjs"><strong>683 tests</strong></a> ensure the software is actually stable</li> <li><strong>387 members</strong> are subscribed to the <a href="https://groups.google.com/forum/#!forum/casperjs">mailing-list</a></li> <li><a href="http://stackoverflow.com/questions/tagged/casperjs"><strong>88 questions</strong></a> have been posted on Stack Overflow about CasperJS</li> <li><a href="https://twitter.com/casperjs_org/followers"><strong>400 people</strong></a> are following the <a href="https://twitter.com/casperjs_org">@casperjs_org</a> twitter account</li> </ul> <p>Btw here's a short excerpt of what people are saying on twitter about it:</p> <p><center> <blockquote class="twitter-tweet"><p>Trying out CasperJS. I am blown away <a href="http://t.co/4sThrr7v" title="http://casperjs.org/">casperjs.org</a></p>&mdash; Beau (@beaumartinez) <a href="https://twitter.com/beaumartinez/status/281467492759646210" data-datetime="2012-12-19T18:34:14+00:00">December 19, 2012</a></blockquote></p> <hr /> <blockquote class="twitter-tweet"><p>Selenium : j'ai jamais pu piffré. Casperjs c'est de la balle ! J'ai installé, et réalisé mes premiers tests en 15 minutes. Merci @<a href="https://twitter.com/n1k0">n1k0</a></p>&mdash; Laurent Jouanneau (@ljouanneau) <a href="https://twitter.com/ljouanneau/status/278278668189593601" data-datetime="2012-12-10T23:22:59+00:00">December 10, 2012</a></blockquote> <hr /> <blockquote class="twitter-tweet" lang="en"><p>Turns out that @<a href="https://twitter.com/phantomjs">phantomjs</a> with casperjs for headless, in-browser integration testing is rather awesome. Recommended.</p>&mdash; Boris Terzic (@boristerzic) <a href="https://twitter.com/boristerzic/status/275920317908656129" data-datetime="2012-12-04T11:11:44+00:00">Décembre 4, 2012</a></blockquote> <hr /> <blockquote class="twitter-tweet"><p>@<a href="https://twitter.com/n1k0">n1k0</a> Thanks for CasperJS. If we ever meet in person, I owe you a beer.</p>&mdash; Stephen Hay (@stephenhay) <a href="https://twitter.com/stephenhay/status/274435181077753856" data-datetime="2012-11-30T08:50:20+00:00">November 30, 2012</a></blockquote> <hr /> <blockquote class="twitter-tweet"><p>sweet goodness -- Phantom.JS + Casper.JS is what we always wanted for testing client-side stuff</p>&mdash; christian verkerk (@chrisverkerk) <a href="https://twitter.com/chrisverkerk/status/273220063761408000" data-datetime="2012-11-27T00:21:54+00:00">November 27, 2012</a></blockquote> <p></center> <p style="text-align:center"><strong><code>&lt;/brag&gt;&lt;realism&gt;</code></strong></p></p> <p>That makes a lot of attention — and therefore expectations — for what started as a <em>tiny helper script on top of PhantomJS</em>… and put a bit of pressure on my shoulders :)</p> <h2>So what's new in CasperJS 1.0?</h2> <p>Lots of stuff, and <a href="https://github.com/n1k0/casperjs/issues?direction=asc&amp;milestone=1&amp;sort=created&amp;state=closed">I mean it</a>. In no particular order:</p> <ul> <li>added a <a href="http://casperjs.org/testing.html#casper-test-command"><code>casperjs test</code> command</a> to run test scripts</li> <li>added support for <a href="http://casperjs.org/selectors.html">CSS3 and XPath selectors</a></li> <li>added support for <a href="http://casperjs.org/api.html#casper.waitForPopup">popups</a> and <a href="http://casperjs.org/api.html#casper.withFrame">(i)frames</a></li> <li>there's now a way to specify how many tests were planned when <code>done()</code> is called</li> <li>easy access of current HTTP response object as the first parameter of step callbacks (thx <a href="https://tiwtter.com/oncletom">oncletom</a>!)</li> <li>many new <a href="http://casperjs.org/api.html#tester">assertions methods</a> have been added to the <code>Tester</code> class</li> <li>added a convenient <a href="http://en.wikipedia.org/wiki/Batch_file">Batch</a> script for Windows users <small>that's where you gotta love your community</small></li> <li>better contextualization of errors, nicer output of them</li> <li>XUnit XML test result logs now contain the duration of each test case</li> <li>added many new <a href="http://casperjs.org/events-filters.html">events and filters</a> so you can hook quite wherever you want into the casper asynchronous workflow</li> <li>better compliance with PhantomJS' native <code>evaluate()</code> argument passing</li> <li>plus many bugfixes, refactors and other enhancements</li> </ul> <p>As a side note:</p> <ul> <li>PhantomJS 1.6.x support has been dropped</li> <li>PhantomJS <a href="http://phantomjs.org/release-1.7.html">1.7</a> &amp; <a href="http://phantomjs.org/release-1.8.html">1.8</a> are supported</li> </ul> <h2>The CasperJS ecosystem</h2> <p>As I realized that opensource projects based on CasperJS were created, I decided to open <a href="https://github.com/casperjs">a dedicated github organization</a>, first for trying to list them in a central place.</p> <p>Some interesting CasperJS-based projects:</p> <ul> <li><strong><a href="https://github.com/WaterfallEngineering/SpookyJS">SpookyJS</a></strong>, which basically allow driving CasperJS from <a href="http://nodejs.org/">Node.js</a></li> <li><strong><a href="https://github.com/ebrehault/resurrectio">resurectio</a></strong>, a <a href="http://www.google.com/chrome">Chrome</a> extension for recording your casper tests</li> <li><strong><a href="https://github.com/brianmhunt/casper-chai">casper-chai</a></strong> extends <a href="http://chaijs.com/">Chai</a> with assertions for CasperJS</li> <li><strong><a href="https://github.com/ronaldlokers/grunt-casperjs">grunt-casperjs</a></strong> and <strong><a href="https://github.com/caiges/grunt-functional">grunt-functional</a></strong>, both based on <a href="http://gruntjs.com/">grunt</a>, allowing to launch your CasperJS tests as well as other javascript build operations in a single command line call</li> <li><strong><a href="https://github.com/Huddle/PhantomCSS">PhantomCSS</a></strong>, which provides integration of js-imagediff with PhantomJS (and CasperJS) for automated visual regression testing</li> <li><strong><a href="https://github.com/acuminous/yadda">yadda</a></strong>, which has just started but is <a href="https://github.com/acuminous/yadda/blob/master/examples/casper/google-scenario.js">genuinely exciting</a>, intends to bring <a href="http://en.wikipedia.org/wiki/Behavior-driven_development">BDD</a> to CasperJS</li> <li>… and many other <a href="https://github.com/casperjs">cool &amp; useful projects</a></li> </ul> <h2>The future</h2> <ul> <li><strong>CasperJS 1.1 is on its way starting today</strong>, and will provide new features and bugfixes, trying not to reinvent the capser wheel</li> <li>CasperJS 2.0 will probably be a big <del>rewrite</del> refactor of the 1.x codebase, and will try to use more of the existing javascript ecosystem (think <a href="http://visionmedia.github.com/mocha/">mocha</a> for instance)… but let's enjoy the 1.x for a while first! <small>and let me have a bit of sleep too</small></li> </ul> <p>See ya.</p> Mon, 24 Dec 2012 00:00:00 +0000 https://nicolas.perriault.net/code/2012/casperjs-hits-1-0-stable/ nicolas+rss@perriault.net (Nicolas Perriault) Le Web au doigt et à l'œil avec CasperJS https://nicolas.perriault.net/talks/2012/sudweb-le-web-au-doigt-et-a-l-oeil-avec-casperjs/ <p>Conférence donnée lors de <a href="http://sudweb.fr/2012/talk/le-web-au-doigt-et-a-loeil-avec-casperjs/">l'édition 2012 de SudWeb</a>&nbsp;:</p> <blockquote> <p>CasperJS est un outil permettant de scripter en Javascript un scénario de navigation sur un véritable navigateur Web, basé sur Webkit et ne nécessitant pas d’interface graphique. CasperJS peut servir à produire des tests fonctionnels poussés bénéficiant d’un environnement DOM et Javascript complet, mais aussi à analyser des documents Web, faire des tests de performance ou même extraire des données.</p> <p>L’intérêt majeur de CasperJS est qu’il permet relativement simplement de tester et valider fonctionnellement des sites et applications Web complexes, reposant sur l’exploitation intensives de Javascript, de comportements asynchrones et d’AJAX par exemple.</p> </blockquote> <iframe src="http://player.vimeo.com/video/49221062" width="658" height="500" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe> <script async class="speakerdeck-embed" data-id="4fc49ec24465dc001f008cb2" data-ratio="1.3333333333333333" src="//speakerdeck.com/assets/embed.js"> <p> <a href="https://speakerdeck.com/u/n1k0/p/le-web-au-doigt-et-a-loeil-avec-casperjs">Lien direct vers la présentation</a> </p> </script> <p> <a href="https://speakerd.s3.amazonaws.com/presentations/4fc49ec24465dc001f008cb2/prez_casperjs.pdf">Télécharger au format PDF</a> </p> <p>Un screencast d'une partie l'atelier qui a suivi cette présentation est également <a href="http://vimeo.com/album/1951235/video/42881484">disponible sur Vimeo</a>&nbsp;:</p> <iframe src="http://player.vimeo.com/video/42881484" width="660" height="381" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe> Thu, 24 May 2012 00:00:00 +0000 https://nicolas.perriault.net/talks/2012/sudweb-le-web-au-doigt-et-a-l-oeil-avec-casperjs/ nicolas+rss@perriault.net (Nicolas Perriault) Nouvelles aventures https://nicolas.perriault.net/carnet/2012/nouvelles-aventures/ <p><strong>Mise à jour du 2 avril :</strong> Ok, on a bien rigolé (ou pas™), mais le poisson était visiblement trop gros. Il faut dire que je n'y suis pas allé de main morte non plus.</p> <p>Pour mémoire, le contenu original du billet :</p> <blockquote> <p>Cela fait quelques mois que je m'interroge sur la pérennité de mon activité en temps qu'indépendant, car force est de constater que la conjoncture ne facilite pas mon approche initiale plutôt artisanale du développement Web.</p> <p>C'est pour cela que lorsque j'ai été contacté par <a href="http://oracle.com/">Oracle</a> pour un éventuel rachat d'<a href="http://akei.com/">Akei</a>, ma société, afin d'intégrer l'expertise autour de <a href="http://casperjs.org/">CasperJS</a>, je n'ai pu m'empêcher d'y voir un signe du destin m'invitant à remettre en questions un certain nombre de principes que je croyais miens jusqu'alors.</p> <p>En effet, j'ai toujours — de façon un peu naïvement idéologique il est vrai — voulu privilégier une approche humaniste du développement logiciel, favorisant les interactions humaines avant tout dans la perspective de faciliter la production de valeur ajoutée. À l'épreuve des faits, force a été de constater que sans une approche épaulée commercialement de l'industrialisation de la production logicielle, tout ceci est condamné à rester lettre morte dans nos sociétés toujours plus assujetties aux vigoureuses et motivantes pressions économiques de toutes parts, particulièrement des pays émergeants, offrant toujours plus pour de moins en moins cher…</p> <p>Aussi, j'integrerai dès lundi <a href="http://www.oracle.com/fr/index.html">Oracle France</a> en tant que Responsable du Pôle Régional de Développement Stratégique et d'Innovation Web au sein des locaux parisiens de la société. J'en profiterai par conséquence pour quitter Montpellier et retrouver la dynamique capitale française, que je suis bien obligé de légitimer comme le seul véritable endroit où <em>les choses se font</em>, comme on dit.</p> <p>En ce qui concerne <a href="http://casperjs.org/">CasperJS</a>, le processus de réécriture en Java a été secrètement entamé depuis quelques semaines, et je pense pouvoir fournir dans les jours qui viennent une première version alpha exploitant l'incroyable puissance de l'<a href="http://www.oracle.com/technetwork/developer-tools/adf/overview/index.html">Oracle Development Framework</a>…</p> <p>… et de tout l'<a href="http://www.marseille-sympa.com/merou.jpg">ecosystème J2EE plus généralement</a>.</p> </blockquote> Sun, 01 Apr 2012 00:00:00 +0000 https://nicolas.perriault.net/carnet/2012/nouvelles-aventures/ nicolas+rss@perriault.net (Nicolas Perriault) Cadaqués at Night https://nicolas.perriault.net/photography/2012/cadaques/ <p><img src="/static/photography/2012/cadaques/main.jpg" alt="Cadaqués at Night by Nicolas Perriault"></p><p>Cadaqués, Spain</p><p>Long exposure shot (25s.) of <a href="http://g.co/maps/fwrh2">Cadaqués</a> at night using the Fuji X100.</p> Mon, 12 Mar 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/cadaques/ nicolas+rss@perriault.net (Nicolas Perriault) Dalí’s Cadillac https://nicolas.perriault.net/photography/2012/dali-s-cadillac/ <p><img src="/static/photography/2012/dali-s-cadillac/main.jpg" alt="Dalí’s Cadillac by Nicolas Perriault"></p><p>Dalí’s Cadillac, Casa Dalí, Figueres, Spain</p><p>Back from an amazing journey to the crazy world of Salvador Dalí.</p> Sun, 11 Mar 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/dali-s-cadillac/ nicolas+rss@perriault.net (Nicolas Perriault) CasperJS, a toolkit on top of PhantomJS https://nicolas.perriault.net/code/2012/introducing-casperjs-toolkit-phantomjs/ <p>It's been quite a while since I posted about the <a href="/code/2011/scrape-and-test-any-webpage-using-phantomjs/">awesomeness of PhantomJS</a>, the <em>scriptable headless WebKit with JavaScript API</em>.</p> <p>In the meanwhile, I started coding a <a href="https://github.com/n1k0/casperjs/commit/133310d814d79db08c3982ee4af31d0a71813b8c">tiny lib</a> to ease the creation of <a href="http://phantomjs.org/">PhantomJS</a> scripts, especially navigation scenarios.</p> <p>Six months later, the lib has gained many more features and is now an entire project on its own; <a href="http://casperjs.org/">CasperJS</a> was born and at the time these lines are written, the code repository has more than <a href="https://github.com/n1k0/casperjs">180 watchers and 32 forks on Github</a>.</p> <p><a href="http://ariya.ofilabs.com/">Ariya Hidayat</a>&nbsp;— the creator of PhantomJS&nbsp;— even says of it:</p> <blockquote> <p>In case you haven’t seen CasperJS yet, go and take a look, it’s an <em>extremely</em> useful companion to PhantomJS. (<a href="http://ariya.ofilabs.com/2012/03/phantomjs-and-travis-ci.html">source</a>)</p> </blockquote> <h2>But what does it do?</h2> <p>I'm far too lazy not to quote <a href="http://casperjs.org/">CasperJS' website</a>:</p> <blockquote> <p>CasperJS is an open source navigation scripting &amp; testing utility written in Javascript and based on PhantomJS — the scriptable headless WebKit engine. It eases the process of defining a full navigation scenario and provides useful high-level functions, methods &amp; syntactic sugar for doing common tasks such as:</p> <ul> <li>defining &amp; ordering browsing navigation steps</li> <li>filling &amp; submitting forms</li> <li>clicking &amp; following links</li> <li>capturing screenshots of a page (or part of it)</li> <li>making assertions on remote DOM</li> <li>logging events</li> <li>downloading resources, including binary ones</li> <li>writing functional test suites, saving results as JUnit XML</li> <li>scraping Web contents</li> </ul> </blockquote> <p>Hell, looks like some new coffee-machine… Let's quickly review some of these features though.</p> <h2>Creating navigation scenarios</h2> <p>Scripting a browsing workflow using Javascript is painfully hard if you intend to use chained callbacks. This kind of code is a nightmare to write, to read, to understand and to maintain:</p> <pre><code>var page = require('webpage').create(); page.open(url1, function(status) { if (status == "fail") phantom.exit(); page.open(url2, function(status) { if (status == "fail") phantom.exit(); page.open(url3, function(status) { if (status == "fail") phantom.exit(); page.open(url4, function(status) { if (status == "fail") phantom.exit(); // Can I stop, now? }); }); }); }); </code></pre> <p>Well, CasperJS solves this kind of problem using a convenient API for dealing with asynchronous stuff:</p> <pre><code>var casper = require('casper').create(); casper.start(url1); casper.thenOpen(url2); casper.thenOpen(url3); casper.thenOpen(url4); casper.run(); </code></pre> <p>Want to simulate the user navigation as if he were clicking through links? No problem:</p> <pre><code>var casper = require("casper").create() casper.start('http://my.blog.tld/'); casper.thenClick('nav#menu a.blog'); casper.thenClick('.posts li a'); casper.then(function() { this.echo('Page url is ' + this.getCurrentUrl()); this.echo('Page title is ' + this.getTitle()); }); casper.run(); </code></pre> <p>Note that you can alternatively use <a href="http://coffeescript.org/">coffeescript</a> to write your scripts:</p> <pre><code>var casper = require("casper").create() casper.start "http://my.blog.tld/" casper.thenClick "nav#menu a.blog" casper.thenClick ".posts li a" casper.then -&gt; @echo "Page url is #{@getCurrentUrl()}" @echo "Page title is #{@getTitle()}" casper.run() </code></pre> <h2>Filling and handling forms</h2> <p>Filling and submitting a form is not much harder:</p> <pre><code>casper.start('http://admin.domain.tld/login/', function() { this.fill('form[id="login-form"]', { 'username': 'chuck', 'password': 'n0rr1s' }, true); }); casper.then(function() { this.echo(this.getTitle()); }); casper.run(); </code></pre> <h2>Capturing screenshots</h2> <p>Capturing a screenshot of a given area is as easy as this:</p> <pre><code>casper.start('http://domain.tld/page.html', function() { this.captureSelector('capture.png', '.article-content'); }); casper.run(); </code></pre> <h2>Asynchronous rendering of page</h2> <p>Sometimes (ok, often), lots of stuff is loaded asynchronously through Ajax or any other fancy invention of the Devil. You can just wait for it to happen:</p> <pre><code>casper.start('https://twitter.com/casperjs_org', function() { this.waitForSelector('.tweet-row', function() { this.captureSelector('twitter.png', 'html'); }, function() { this.die('Timeout reached. Fail whale?').exit(); }, 2000); }); </code></pre> <h2>Testing</h2> <p>Now all of this is fancy, but the true powers of CasperJS come from its functional testing capabilities. For example, testing google search can be done that way:</p> <pre><code>casper.start('http://www.google.fr/', function() { this.test.assertTitle('Google', 'google homepage title is the one expected'); this.test.assertExists('form[action="/search"]', 'main form is found'); this.fill('form[action="/search"]', { q: 'foo' }, true); }); casper.then(function() { this.test.assertTitle('foo - Recherche Google', 'google title is ok'); this.test.assertUrlMatch(/q=foo/, 'search term has been submitted'); this.test.assertEval(function() { return __utils__.findAll('h3.r').length &gt;= 10; }, 'google search for "foo" retrieves 10 or more results'); }); casper.run(function() { this.test.renderResults(true); }); </code></pre> <p>Running the suite would produce this shiny colored output:</p> <p><img alt="" src="/static/code/2012/testsuiteok.png" /></p> <p>As a bonus, these results can be exported as XUnit XML, eg. for being consumed by a continuous integration server like <a href="http://jenkins-ci.org/">Jenkins</a>.</p> <p>For the records, the whole CasperJS test suite is written using its own API, and results are <a href="http://travis-ci.org/#!/n1k0/casperjs">visible on Travis-CI</a>.</p> <h2>Now, what?</h2> <p>Nothing, really. If you think it's useful, I'm glad enough.</p> <p><a href="http://casperjs.org/">CasperJS website</a>.</p> Thu, 08 Mar 2012 00:00:00 +0000 https://nicolas.perriault.net/code/2012/introducing-casperjs-toolkit-phantomjs/ nicolas+rss@perriault.net (Nicolas Perriault) Dead easy yet powerful static website generator with Flask https://nicolas.perriault.net/code/2012/dead-easy-yet-powerful-static-website-generator-with-flask/ <p>It's been a long time I wanted to federate my online identities in a single, managed place — hence the website you're currently browsing. I've also been looking for a static website builder for some times, trying many and retaining zero. It was a bit depressing, frustrating to say the least.</p> <p>Then I encountered <a href="https://twitter.com/#!/mitsuhiko/statuses/166570613295689728">this tweet by Armin Ronacher</a>:</p> <blockquote class="twitter-tweet tw-align-center"> <p>Frozen-Flask is really, really useful. Should have used that earlier.</p>&mdash; Armin Ronacher (@mitsuhiko) <a href="https://twitter.com/mitsuhiko/status/166570613295689728" data-datetime="2012-02-06T17:15:03+00:00">February 6, 2012</a> </blockquote> <p><a href="http://lucumr.pocoo.org/">Armin</a> is the author of <a href="http://flask.pocoo.org/">Flask</a>, a Python microframework I much appreciate for its simplicity, so this tweet immediately rang a bell and I started exploring the possibilities offered by <a href="http://packages.python.org/Frozen-Flask/">Frozen-Flask</a>.</p> <p>Frozen-Flask basically <em>freezes</em> a Flask application into a set of static files, so you can host it without pain, but with speed. Combined with <a href="http://packages.python.org/Flask-FlatPages/">Flask-FlatPages</a>, you got the perfect set for generating your static website with everything you could expect by using a framework:</p> <ul> <li>cool uris &amp; easy routing management</li> <li>powerful templating</li> <li>local, dynamic serving</li> <li>static version generation</li> </ul> <h2>First iteration: project setup</h2> <p>Create a brand new <a href="http://pypi.python.org/pypi/virtualenv">virtualenv</a> in a new directory and install the necessary packages using <a href="http://pypi.python.org/pypi/pip">pip</a>:</p> <pre><code>$ mkdir sample_project &amp;&amp; cd !$ $ mkvirtualenv --no-site-packages `pwd`/env $ source env/bin/activate $ pip install Flask Frozen-Flask Flask-FlatPages </code></pre> <p>Write a very first version of our app in a <code>sitebuilder.py</code> file:</p> <pre><code>from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello World!" if __name__ == "__main__": app.run(port=8000) </code></pre> <p>Run it; you should see someting like:</p> <pre><code>$ python sitebuilder.py * Running on http://127.0.0.1:8000/ * Restarting with reloader </code></pre> <p>Ensure with your browser that everyting is going fine by heading at <code>http://127.0.0.1:8000/</code>.</p> <h2>New iteration: adding <em>flat pages</em></h2> <p><a href="http://packages.python.org/Flask-FlatPages/">Flask-FlatPages</a> provides a collection of pages to your Flask application. Pages are built from <em>flat</em> text files as opposed to a relational database.</p> <p>Create a <code>pages/</code> directory at the root of your project folder, and put a <code>hello-world.md</code> file in there:</p> <pre><code>$ mkdir pages $ vi pages/hello-world.md </code></pre> <p>The <code>pages/hello-world.md</code>:</p> <pre class="unpretty"><code>title: Hello World date: 2012-03-04 **Hello World**, from a *page*! </code></pre> <p>As you can see, we can write plain <a href="http://daringfireball.net/projects/markdown/">Markdown</a> for our page contents. So let's rewrite our app to serve any flatpage by its filename:</p> <pre><code>from flask import Flask from flask_flatpages import FlatPages DEBUG = True FLATPAGES_AUTO_RELOAD = DEBUG FLATPAGES_EXTENSION = '.md' app = Flask(__name__) app.config.from_object(__name__) pages = FlatPages(app) @app.route('/') def index(): return "Hello World" @app.route('/&lt;path:path&gt;/') def page(path): return pages.get_or_404(path).html if __name__ == '__main__': app.run(port=8000) </code></pre> <p>Now requesting <code>http://127.0.0.1:8000/hello-world/</code> will display our flatpage. Note that the markdown source is converted to html by getting the <code>html</code> property of our page object.</p> <h2>New iteration: adding templates</h2> <p>Flask uses the <a href="http://jinja.pocoo.org/docs/">Jinja2</a> template engine, so let's create some templates to decorate our pages. First create a <code>templates</code> directory at the root of the project:</p> <pre><code>$ mkdir templates </code></pre> <p>Create a base layout in <code>templates/base.html</code>:</p> <pre><code>&lt;!doctype html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;My site&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;&lt;a href="{{ url_for("index") }}"&gt;My site&lt;/a&gt;&lt;/h1&gt; {% block content %} &lt;p&gt;Default content to be displayed&lt;/p&gt; {% endblock content %} &lt;/body&gt; &lt;/html&gt; </code></pre> <p>Note the use of the <code>url_for()</code> template helper, that's the way we generate urls using Flask and Jinja2.</p> <p>Now the <code>page.html</code> template to fill this layout with page contents:</p> <pre><code>{% extends "base.html" %} {% block content %} &lt;h2&gt;{{ page.title }}&lt;/h2&gt; {{ page.html|safe }} {% endblock content %} </code></pre> <p>Our app is now:</p> <pre><code>from flask import Flask, render_template from flaskext.flatpages import FlatPages DEBUG = True FLATPAGES_AUTO_RELOAD = DEBUG FLATPAGES_EXTENSION = '.md' app = Flask(__name__) app.config.from_object(__name__) pages = FlatPages(app) @app.route('/') def index(): return "Hello World" @app.route('/&lt;path:path&gt;/') def page(path): page = pages.get_or_404(path) return render_template('page.html', page=page) if __name__ == '__main__': app.run(port=8000) </code></pre> <p>Hell, what did we just do?</p> <ul> <li>we created templates for our app; a common layout (<code>base.html</code>) and a page template (<code>page.html</code>)</li> <li>we used the <code>render_template</code> function to use the page template for decorating our pages</li> <li>the page template extends the base one to avoid copying and pasting stuff for every page on Earth</li> </ul> <h2>New iteration: display the list of available pages on the homepage</h2> <p>For now our homepage has been a bit sick, to say the least. Let's make it an index of all available pages.</p> <p>Create a <code>templates/index.html</code> with the following contents:</p> <pre><code>{% extends "base.html" %} {% block content %} &lt;h2&gt;List of stuff&lt;/h2&gt; &lt;ul&gt; {% for page in pages %} &lt;li&gt; &lt;a href="{{ url_for("page", path=page.path) }}"&gt;{{ page.title }}&lt;/a&gt; &lt;/li&gt; {% else %} &lt;li&gt;No stuff.&lt;/li&gt; {% endfor %} &lt;/ul&gt; {% endblock content %} </code></pre> <p>Feel free to create more flat pages, the same way we did with <code>hello-world.md</code>, storing the file into the <code>pages/</code> directory using the <code>.md</code> extension.</p> <p>So the <code>index()</code> route of our app is now:</p> <pre><code>@app.route('/') def index(): return render_template('index.html', pages=pages) </code></pre> <p>If you reload the homepage, a list of all available flatpages is displayed with a link for each. Damn, that was pretty easy.</p> <h2>New iteration: adding metadata to pages</h2> <p>Flask-FlatPages allows to enter metadata for pages the same way we did for the title and the creation date with our <code>hello-world.md</code>, and access them using the <code>page.meta</code> property, which contains a plain silly python dict. That's pretty awesome when you think about it, heh?</p> <p>So let's imagine you want tags for your pages, our <code>hello-world.md</code> becomes:</p> <pre class="unpretty"><code>title: Hello World date: 2012-03-04 tags: [general, awesome, stuff] **Hello World**, from a *page*! </code></pre> <p>For the records, metadata are described in <a href="http://yaml.org/">YAML</a>, so you can use strings, booleans, integers, floats, lists and even dicts which will be converted to their respective native Python equivalent.</p> <p>We're going to use two different templates for listing general pages and tagged ones, using a shared template partial. Our <code>index.html</code> is now:</p> <pre><code>{% extends "base.html" %} {% block content %} &lt;h2&gt;List of stuff&lt;/h2&gt; {% with pages=pages %} {% include "_list.html" %} {% endwith %} {% endblock content %} </code></pre> <p>Create a new <code>tag.html</code> template, which will display the list of tagged pages:</p> <pre><code>{% extends "base.html" %} {% block content %} &lt;h2&gt;List of stuff tagged &lt;em&gt;{{ tag }}&lt;/em&gt;&lt;/h2&gt; {% with pages=pages %} {% include "_list.html" %} {% endwith %} {% endblock content %} </code></pre> <p>The new <code>_list.html</code> template we need for inclusion contains:</p> <pre><code>&lt;ul&gt; {% for page in pages %} &lt;li&gt; &lt;a href="{{ url_for("page", path=page.path) }}"&gt;{{ page.title }}&lt;/a&gt; {% if page.meta.tags|length %} | Tagged: {% for page_tag in page.meta.tags %} &lt;a href="{{ url_for("tag", tag=page_tag) }}"&gt;{{ page_tag }}&lt;/a&gt; {% endfor %} {% endif %} &lt;/li&gt; {% else %} &lt;li&gt;No page.&lt;/li&gt; {% endfor %} &lt;/ul&gt; </code></pre> <p>Let's add a new <code>tag</code> route to our app, to use our new <code>tag.html</code> template:</p> <pre><code>@app.route('/tag/&lt;string:tag&gt;/') def tag(tag): tagged = [p for p in pages if tag in p.meta.get('tags', [])] return render_template('tag.html', pages=tagged, tag=tag) </code></pre> <p><strong>Note:</strong> if you didn't like Python's <a href="http://www.network-theory.co.uk/docs/pytut/ListComprehensions.html">list comprehensions</a> yet, now you do.</p> <h2>New iteration: generate the static stuff</h2> <p>Well, for now we only have a dynamic website, which uses and serves flatpages stored on the filesystem: CRAP. But the idea's of course not to host a Flask app but a set of static files and assets to skip the need of any application server.</p> <p>Here enters Frozen-Flask. Its use is damn easy:</p> <pre><code>import sys from flask import Flask, render_template from flask_flatpages import FlatPages from flask_frozen import Freezer DEBUG = True FLATPAGES_AUTO_RELOAD = DEBUG FLATPAGES_EXTENSION = '.md' app = Flask(__name__) app.config.from_object(__name__) pages = FlatPages(app) freezer = Freezer(app) @app.route('/') def index(): return render_template('index.html', pages=pages) @app.route('/tag/&lt;string:tag&gt;/') def tag(tag): tagged = [p for p in pages if tag in p.meta.get('tags', [])] return render_template('tag.html', pages=tagged, tag=tag) @app.route('/&lt;path:path&gt;/') def page(path): page = pages.get_or_404(path) return render_template('page.html', page=page) if __name__ == '__main__': if len(sys.argv) &gt; 1 and sys.argv[1] == "build": freezer.freeze() else: app.run(port=8000) </code></pre> <p>And run:</p> <pre><code>$ python sitebuilder.py build </code></pre> <p>Open the <code>build</code> folder generated by this command:</p> <pre><code>$ tree . ├── hello-world │   └── index.html ├── index.html └── tag ├── awesome │   └── index.html ├── general │   └── index.html └── stuff └── index.html 5 directories, 5 files </code></pre> <p><strong>MIND: BLOWN.</strong></p> <p>You can now deploy the contents of this <code>build</code> directory to any webserver which's able to serve static files, and you're done. With just 34 lines of manually written Python code… not bad heh?</p> <p>Of course, our website is pretty crappy right now, but you should be get started to add features on your own, now.</p> Sun, 04 Mar 2012 00:00:00 +0000 https://nicolas.perriault.net/code/2012/dead-easy-yet-powerful-static-website-generator-with-flask/ nicolas+rss@perriault.net (Nicolas Perriault) Nouveau site https://nicolas.perriault.net/carnet/2012/nouveau-site/ <p><strong>J'avais envie de fédérer l'ensemble de ma <em>production numérique</em> au sein d'un seul et même site, c'est donc chose faite si vous lisez ces quelques lignes.</strong></p> <p>J'ai essayé de résumer au maximum mes activités principales, du moins celles que je trouve potentiellement dignes d'intérêt pour autrui et sur lesquelles j'ai envie de communiquer publiquement&nbsp;:</p> <ul> <li>le <a href="/code">développement Web</a>, en anglais, où je parlerai de ce qui à trait à la technicité de mon métier&nbsp;;</li> <li>la <a href="/photography">photographie</a>, où je publierai les photos que je trouve les plus intéressantes&nbsp;;</li> <li>mes <a href="/talks">interventions publiques en conférence</a>, plus à des fins d'archivage personnel que pour faire reluire mon ego&nbsp;;</li> <li>pour toutes les autres choses qui me passent par la tête, un <a href="/carnet">carnet en français</a> fera vraisemblablement l'affaire. La preuve.</li> </ul> <h2>La carrosserie</h2> <p>Je voulais quelque chose de simple, efficace et épuré au maximum. Le contenu textuel est largement mis en avant, sauf dans le cas des photographies où l'image prime bien évidemment.</p> <p>J'ai choisi la police <em>Palatino</em>, parce qu'elle sied à mon regard et qu'elle est disponible sur la plupart des plateformes, mobile y compris.</p> <p>J'ai essayé de faire attention à l'importance de l'espace, de mettre en valeur le vide et le rythme typographique, même si je ne suis pas encore pleinement satisfait du résultat. En même temps, c'est comme ça qu'on apprend.</p> <p><em>Note: Je n'ai pas le projet à court terme de rendre ce site compatible avec les caprices du moteur de rendu d'Internet Explorer. J'ose espérer que la version 9 du navigateur ne maltraite pas trop l'affichage, mais d'expérience, rien n'est moins sûr. Peu importe.</em></p> <h2>Sous le capot</h2> <p>Je n'ai utilisé aucun framework CSS, juste quelques styles bien sentis. Et ça fait plutôt pas mal le taf, en restant très léger et maintenable notamment grâce à <a href="http://lesscss.org/">LESS</a>.</p> <p>Côté moteur, j'ai développé un petit générateur de site statique sur base <a href="http://flask.pocoo.org/">Flask</a>, un microframework en Python que je vous enjoins à considérer tellement il est efficace et bien documenté. Mais je vous en dirai un peu plus dans un prochain billet technique à paraître prochainement.</p> <p>Les billets sont écrits en <a href="http://daringfireball.net/projects/markdown">markdown</a>, une syntaxe simple et limitée (d'aucuns suggéreront que qui se ressemble s'assemble, mais j'emmerde d'aucuns).</p> <h2>À venir</h2> <p>Il me reste pas mal de photos à importer et quelques redirections permanentes à mettre en place, mais je suis globalement plutôt satisfait du résultat. Surtout, je vais pouvoir me remettre à écrire autre chose que des <a href="https://twitter.com/n1k0"><em>tweets</em></a>, et ça c'est plutôt cool.</p> Sat, 03 Mar 2012 00:00:00 +0000 https://nicolas.perriault.net/carnet/2012/nouveau-site/ nicolas+rss@perriault.net (Nicolas Perriault) Gandi Standard SSL Certificate & Nginx https://nicolas.perriault.net/code/2012/gandi-standard-ssl-certificate-nginx/ <p><a href="http://gandi.net/">Gandi</a> offers a <a href="http://wiki.gandi.net/en/ssl/free">free SSL certificate</a> during one year for any domain you buy there, nice. But the setup is a bit tedious and the documentation a bit disparate, so here's an attempt for a comprehensive howto for configuring a secured <a href="http://nginx.org/">nginx</a> vhost using your <em>free</em> certificate.</p> <p><strong>Disclaimer:</strong> <em>Gandi's quite complicated email confirmation and validation workflow won't be covered in this post. Just ensure you can receive emails at <code>admin@domain.tld</code> where <code>domain.tld</code> is your domain.</em></p> <h2>Creating your certificate</h2> <p>On your server, ensure you have the <a href="http://openssl.org/"><code>openssl</code></a> command available or install it. Then, <a href="http://wiki.gandi.net/en/ssl/csr">generate your CSR</a> (<code>domain.tld</code> is your actual domain):</p> <pre><code>$ openssl req -nodes -newkey rsa:2048 -keyout domain.tld.key \ -out domain.tld.csr </code></pre> <p>Answer a few questions from the program, but here's the most important bit: <strong>enter your domain or subdomain when prompted to provide a <em>Common Name</em></strong>:</p> <pre><code>Common Name (eg, YOUR name) []: domain.tld </code></pre> <p>As they say:</p> <blockquote> <p>The process will create 2 files: a public <code>.csr</code> file, and a private <code>.key</code> file which you must absolutely keep private.</p> </blockquote> <p><strong>Note:</strong> <em>Alas, you won't be able to extend your certificate to all possible subdomains using a wildcard as it is <a href="http://wiki.gandi.net/questions/fr/ssl/csr/multi-domaine-non-accepte">only supported starting with their <em>«Pro»</em> offer</a>.</em></p> <p>Have a coffee.</p> <hr /> <h2>Enter <del>Sandman</del> Franz Kafka</h2> <p>Next, head to your <a href="https://www.gandi.net/admin/domain">Gandi domain management page</a>, select your domain, click on <em>SSL Certificate: Manage</em>, click on <em>Activate this certificate</em> then Paste the content of the generated <code>domain.tld.csr</code> file into the textarea and submit the form. Then wait for some email from Gandi to confirm your demand.</p> <p>In the meanwhile, <a href="http://wiki.gandi.net/en/ssl/intermediate">retrieve the intermediate certificate</a> from the <a href="https://www.gandi.net/admin/ssl/manage">SSL certificate management page</a> of your Gandi account, clicking on the tiny magnifying glass next to your domain name (the one with a nicely hidden <em>«Get the certificate»</em> tooltip on rollover); you'll get both the certificate and <em>Gandi's operational certificate authority</em> files:</p> <ul> <li><code>cert-domain.tld.pem</code>, where <code>domain.tld</code> is your actual domain</li> <li><code>GandiStandardSSLCA.pem</code></li> </ul> <p>Upload them both to your server, store them in eg. <code>/etc/nginx/certificates/</code> (nope, this directory is unlikely to exist by default; do as you like).</p> <p>Last, you have to append the Gandi CA to your domain certificate:</p> <pre><code>$ cat GandiStandardSSLCA.pem &gt;&gt; cert-domain.tld.crt </code></pre> <p>Have a capuccino.</p> <hr /> <h2>Configuring the nginx vhost</h2> <p>Here's a sample vhost server configuration for nginx, kept as concise as possible for the sake of brevity and clarity:</p> <pre><code>server { listen 443; server_name "domain.tld"; root /var/www/your_website_root; ssl on; ssl_certificate /etc/nginx/certificates/cert-domain.tld.crt; ssl_certificate_key /etc/nginx/certificates/domain.tld.key; } </code></pre> <p>Restart nginx and you should be able to access your website using <code>https://domain.tld/</code>.</p> <p>To redirect all HTTP trafic to HTTPS for this server, add this:</p> <pre><code>server { listen 80; server_name "domain.tld"; rewrite ^(.*) https://$host$1 permanent; } </code></pre> <p>Have a latte.</p> <h2>Conclusion</h2> <p>At this point, you should be seriously considering quitting caffeine. As a positive note, it looks like the whole process will be revamped in <em>some weeks</em>:</p> <blockquote class="twitter-tweet tw-align-center" data-in-reply-to="174864441731588096"> <p>@<a href="https://twitter.com/n1k0">n1k0</a> browsers (ff and others) allow us to validate via a DNS entry we will change the workflow in some weeks when we'll put it in the API.</p>&mdash; gandi.net (@gandi_net) <a href="https://twitter.com/gandi_net/status/174909743612166144" data-datetime="2012-02-29T17:31:47+00:00">February 29, 2012</a> </blockquote> <p>If you're looking for other alternatives than Gandi for getting SSL certs, it seems that many people are speaking well of <a href="http://www.startssl.com/">StartSSL</a> despite their ugly site, but I didn't use the service myself.</p> <hr /> <h2>References</h2> <ul> <li><a href="http://wiki.gandi.net/en/ssl">Gandi SSL wiki pages</a></li> <li><a href="http://www.informathic.com/post/2010/12/24/installer-ssl-gandi-nginx">Installer un certificat Gandi SSL Standard sur Nginx</a> (French)</li> </ul> Wed, 29 Feb 2012 00:00:00 +0000 https://nicolas.perriault.net/code/2012/gandi-standard-ssl-certificate-nginx/ nicolas+rss@perriault.net (Nicolas Perriault) L’Espiguette https://nicolas.perriault.net/photography/2012/espiguette/ <p><img src="/static/photography/2012/espiguette/main.jpg" alt="L’Espiguette by Nicolas Perriault"></p><p>Plage de l'Espiguette, janvier 2012</p><p>Some oldies I never took the time to publish.</p> Wed, 15 Feb 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/espiguette/ nicolas+rss@perriault.net (Nicolas Perriault) Communism is Dead https://nicolas.perriault.net/photography/2012/communism-is-dead/ <p><img src="/static/photography/2012/communism-is-dead/main.jpg" alt="Communism is Dead by Nicolas Perriault"></p><p>Signs of the times?</p> Tue, 14 Feb 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/communism-is-dead/ nicolas+rss@perriault.net (Nicolas Perriault) Follow the Black Dog https://nicolas.perriault.net/photography/2012/follow-the-black-dog/ <p><img src="/static/photography/2012/follow-the-black-dog/main.jpg" alt="Follow the Black Dog by Nicolas Perriault"></p><p>Small street in Sommières, France</p><p>White rabbits are overrated.</p> Tue, 14 Feb 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/follow-the-black-dog/ nicolas+rss@perriault.net (Nicolas Perriault) Mystery Door https://nicolas.perriault.net/photography/2012/mystery-door/ <p><img src="/static/photography/2012/mystery-door/main.jpg" alt="Mystery Door by Nicolas Perriault"></p> Sat, 11 Feb 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/mystery-door/ nicolas+rss@perriault.net (Nicolas Perriault) Presqu’île de Maguelone https://nicolas.perriault.net/photography/2012/presqu-ile-de-maguelone/ <p><img src="/static/photography/2012/presqu-ile-de-maguelone/main.jpg" alt="Presqu’île de Maguelone by Nicolas Perriault"></p> Mon, 02 Jan 2012 00:00:00 +0000 https://nicolas.perriault.net/photography/2012/presqu-ile-de-maguelone/