Google Chrome ja Protractor timeout mysteeri ratkennut

Kirjailin taannoin tänne testiautomaatiota riivanneesta pulmasta, kun yllättäen Chrome selaimella ajettavat Protractor testit alkoivat epäonnistua timeout-virheiden vuoksi. Syyksi paljastui Chrome version 38 bugi suhteessa Selenium Webdriveriin. Tilapäinen korjaus jonka taisin tänne myös kirjoitella oli poistaa koneesta Chrome 38, asentaa tilalle Chrome 37, ja sitten disabloida auto-update.

Bugi johon tässä viittaan löytyy täältä: https://code.google.com/p/chromium/issues/detail?id=422218

Chrome 39 with updates disabled

Nyt on tuoreeltaan julkaistu uusi Chrome versio 39, jossa tuo bugi on korjattu, ja sain juuri verifioitua että homma toimii taas silläkin ihan aikuisten oikeesti. Eli protractorin kanssa puljatessa homma menee nyt näin: Chrome 39 asennettuna, ja päivitykset edelleen disabloituna. Tästedes päivitetään manuaalisesti.

Tuossa on linkki paikkaan jossa muun muassa käydään läpi tuota auto-päivitysten disablointia:

http://watirmelon.com/2014/11/05/lock-down-your-browser-versions-if-you-run-webdriver-tests/

Huomattakoon että halutessaan voi myös editoida rekisteriä käsin.

Tuossa on Chrome 39 changelog: http://googlechromereleases.blogspot.fi/2014/11/stable-channel-update_18.html

Chrome 39 on muuten myös nopeampi, ja sisältää muitakin bugikorjauksia. Saatavana myös Androidille! 😉

Mainokset

AngularJS, Animate, ja Material slideshow

No niin, tulin kirjailleeksi Angular-appsin korvaamaan powerpoint kalvosulkeisia jollain joka toimii kivasti selaimessa, vaikkapa ipadissa. Tarkasti ottaen modifioin olemassaolevaa täysin hyvää koodia, lisäten siihen muutaman oman mausteen. Originaali artikkeli ja koodi löytyy täältä:

http://onehungrymind.com/build-a-full-page-angularjs-slideshow/

Mitäpä modasin tästä? No, originaali artikkelissa oli $timer palvelulla ajastetut slidet, halusin itse vaihtaa nappia painamalla. Lisäksi halusin ottaa Material Design kirjastot mukaan jatkokehitystä varten, ja käyttää sieltä tulevaa primary button-ideaa. Ja tietysti päivitellä kirjastot tuoreimpiin. Lisäksi kokeilin tehdä tämän alusta alkaen Netbeans HTML5 projektina. Ja täytyy sanoa että sekin puoli alkaa hiljalleen olemaan toimiva.

Joka tapauksessa, jahka projektirunko on luotu (esim. Netbeans HTML5 projekti ja angular seed), on aika päivittää kirjastoriippuvuudet bower.jsoniin:

{
 "name": "angular-seed",
 "description": "Spagettikoodi slideshow presentation",
 "version": "0.0.1",
 "private": true,
 "dependencies": {
 "angular": "1.3.x",
 "angular-route": "1.3.x",
 "angular-loader": "1.3.x",
 "angular-animate": "1.3.x",
 "angular-mocks": "~1.3.x",
 "angular-material": "~0.5.x",
 "bootstrap":"*",
 "angular-bootstrap":"*",
 "gsap": "~1.14.x",
 "PreloadJS": "*",
 "material-design-icons": "*"
 },
 "resolutions": {
 "angular": "~1.3.x"
 }
 }

Seuraava tempaus: Bower lykkää tiedostot juuren alle bower_components kansioon, kun taas Netbeans sisäänrakennettu serveri ei osaa sieltä hakea tiedostoja. Tein siis muutoksen jossa bower lykkääkin hakemansa tiedostot app/lib alle, tekemällä .bowerrc tiedoston jonka sisältö on:

{
 "directory" : "app/lib"
 }

Nyt kun ajan bower install operaation, tarvittavat kirjastot tiukkuvat app/lib kansion alle – jonka muuten lisään .gitignoreen samalla.

Seuraava steppi on ladata javascript kirjastot. Tässä projektissa en käyttänyt requirea joten lataus menee ihan näin:

<script src="lib/jquery/dist/jquery.js"></script>
 <script src="lib/angular/angular.js"></script>
 <script src="lib/angular-route/angular-route.js"></script>
 <script src="lib/angular-animate/angular-animate.js"></script>
 <script src="lib/gsap/src/uncompressed/TweenMax.js"></script>
 <script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>
 <script src="lib/PreloadJS/lib/preloadjs-0.4.1.combined.js"></script>
 <script src="lib/angular-aria/angular-aria.js"></script>
 <script src="lib/hammerjs/hammer.js"></script>
 <script src="lib/angular-material/angular-material.js"></script>
 <script src="js/app.js"></script>

Tämän ohella on hyvä latailla material design tyylisivut ja haluamasi teema, esim. näin:

<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css"/>
 <link rel="stylesheet" href="./lib/angular-material/angular-material.css"/>
 <link rel="stylesheet" href="./lib/angular-material/themes/default-theme.css"/>
 <link rel="stylesheet" href="./css/app.css"/>

Nyt saakin koota sitten index.html sivun body-tagin sisälle slideshow käyttöliittymä. Se menee taas näin:

<div>

<div class="img-container">
 <img ng-repeat="slide in slides"
 class="fullBg {{currentAnimation}}"
 ng-swipe-right="nextSlide()"
 ng-swipe-left="prevSlide()"
 ng-if="isCurrentSlideIndex($index)"
 ng-src="{{slide.image}}"
 >
</div>

<md-button
 aria-label="Next"
 style="position: absolute; right: 3em; top:2em;"
 class="md-fab md-primary"
 ng-click="nextSlide()"
 >
 <md-icon icon="lib/material-design-icons/navigation/svg/production/ic_arrow_forward_24px.svg"
 style="width: 24px; height: 24px;">
 </md-icon>
</md-button>
</div>

No niin, pari huomiota tuosta koodista:

– Päätyön tekee tuo kuva joka hakee ng-repeat direktiivillä controllerin scope-muuttujista slides-muuttujalta kuvien nimet, ja lataa ne. ng-if vuorostaan näyttää valittuna olevan, css luokka määrittää täysikokoisen kuvan sekä valitsee halutun animaation käyttöön siirtymiin. Tästä koodista voi kiittää originaalia 100%.

– Alla on kokeiluluontoinen md-button. Sliden vaihtamisen voi näprätä mihin hyvänsä muuhunkin muotoon, mutta tässä oli tarkoitus testata materialia, joten otetaan pyöreä fab-nappi, primary tehostein. Jos haluaa kokeilla muita teemoja, voi lisätä tähän esim. md-theme=indigo tms – mutta sitten on myös syytä ladata vastaavat css-tyylisivut.

– Button käyttää myös yhtä material-icons kirjaston valmis-ikoneista.

Homma alkaa kasaantumaan. Tämähän vaatii myös tyylisivun. En tehnyt originaaliin kummempia muutoksia joten se löytyy tuolta alussa olevan linkin takaa, mutta tässä keskeinen kohta, eli miltä .fullBg näyttää:

.fullBg {
 position: fixed;
 width: 100%;
 height: 100%;
 top:0px;
 left: 0px;
 }

No niin, sitten varsinaiseen mielenkiintoiseen kohtaan eli koodiin. Tässä on taustacontrolleri:

'use strict';

var app = angular.module('website', ['ngMaterial','ngAnimate', 'ui.bootstrap']);

// Declare app level module which depends on filters, and services

app.controller('MainCtrl', function ($scope) {

  $scope.slides = [];
  for (var i = 1; i <= 29; i++) {
  $scope.slides.push(
    {
      image: 'images/Slide' + i + '.PNG',
      description: 'Image ' + i
    }
  );
}

$scope.currentIndex = 0;
$scope.currentAnimation = 'slide-left-animation';
//$scope.currentAnimation = 'slide-down-animation';
//$scope.currentAnimation = 'fade-in-animation';

$scope.setCurrentSlideIndex = function (index) {
 $scope.currentIndex = index;
};

$scope.isCurrentSlideIndex = function (index) {
 return $scope.currentIndex === index;
};

$scope.nextSlide = function () {
 $scope.currentIndex = ($scope.currentIndex < $scope.slides.length - 1) ? ++$scope.currentIndex : 0;
};

$scope.prevSlide = function () {
 $scope.currentIndex = ($scope.currentIndex > 0) ? --$scope.currentIndex : $scope.slides.length - 1;
};

$scope.setCurrentAnimation = function (animation) {
 $scope.currentAnimation = animation;
};

$scope.isCurrentAnimation = function (animation) {
 return $scope.currentAnimation === animation;
};

$scope.keyPress = function (keyEvent) {
 $scope.nextSlide();
};

});

Jep, eli alussa ladataan slide-kuvia 29 kappaletta. Nollataan indeksi ja valitaan animaatiotyyli. Sitten määritellään muutama funktio – lähinnä set/get funktioita tilanhallintaan – niillä näprätään mikä on valittuna oleva slide ja millä ehdoilla edetään tai peruutetaan, ja mikä on tämänhetkinen animaatio.

Sitten päästään itse mielenkiintoiseen: miten angular-animate toimii. Sillä voi määritellä animaatio-tyyliluokkia, ja kun html elementille on sellainen määritelty, se määrittää mitä tapahtuu esim. elementin poistuessa tai ilmestyessä säiliöön. Esim. jos kuvallemme on annettu, kuten yllä, tällainen tyylimääritys:

<img ng-repeat="slide in slides"
 class="fullBg slide-left-animation"
 ...

..niin kuvan poistuessa näkyvistä tai ilmestyessä ajetaan seuraava animaatio, jonka voit myös määritellä controllerille:

app.animation('.slide-left-animation', function ($window) {
 return {
 enter: function (element, done) {
 TweenMax.fromTo(element, 1, {left: $window.innerWidth}, {left: 0, onComplete: done});
 },
 leave: function (element, done) {
 TweenMax.to(element, 1, {left: -$window.innerWidth, onComplete: done});
 }
 };
});

Se tekee mitä vain haluat. Tässä siis sisääntullessa ajetaan TweenMax.fromTo funktio (GreenSock kirjastosta, joka aiemmin otettiin käyttöön) – ja se animoi siirtymän oikealta vasemmalle (kuvan vasen reuna on aluksi ruudun oikean reunan ulkopuolella, sitten siirtyy sieltä nollaan).

Vastaavasti kun kuva poistuu säiliöstä, se lipuu ikkunan koon verran vasemmalle ruudun ulkopuolelle.

Muita vastaavia animaatiovaihtoehtoja:

app.animation('.slide-down-animation', function ($window) {
 return {
 enter: function (element, done) {
 TweenMax.fromTo(element, 1, {top: -$window.innerHeight}, {top: 0, onComplete: done});
 },
 leave: function (element, done) {
 TweenMax.to(element, 1, {top: $window.innerHeight, onComplete: done});
 }
 };
 });

app.animation('.fade-in-animation', function ($window) {
 return {
 enter: function (element, done) {
 TweenMax.fromTo(element, 1, {opacity: 0}, {opacity: 1, onComplete: done});
 },
 leave: function (element, done) {
 TweenMax.to(element, 1, {opacity: 0, onComplete: done});
 }
 };
});

Mutta tosiaan, ei ole omaa keksintöä, kunhan ihastelen ja sovellan ratkaisua. Angular-animate on hyvin mielenkiintoinen kirjasto ja varovaisen tutustumisen perusteella myös vakaa. Angular-Material tutkinta jatkuu, ei voi sanoa että tässä olisi käytetty sen parhaita piirteitä ja ainakin omalla ruudulla tuo oikean yläkulman fab-nappi on penteleen ruma (sen saa sulautumaan taustaan jos jättää primary luokan pois määrittelystä) – mutta tämä oli itselle enempi tech demo edelleen.

Joka tapauksessa, taas koodia ja linkkivinkkejä itselle muistiin.

 

 

Protractor, Firefox ja InvalidElementStateError

No niin, aina oppii jotain uutta ja ihmeellistä. Protractor e2e testiautomaation pyörintä on vähän viime viikkoina yskinyt. Ensimmäinen ongelma oli automaatioserverin timeoutit, joiden syyksi paljastui jonkunmoinen bugi chrome selaimessa/sen driverissa.

Korjaukseksi otettiin käyttöön Firefox odotellessa Chromen palautumista toimintakuntoiseksi. Firefox ajoi iloisesti kaikki muut testit paitsi ne joissa valitaan selection listasta joku tietty arvo sisällön perusteella. Originaali koodi:

//Open selection box
selectBoxElement.click();
browser.waitForAngular();
selectBoxElement.element(by.cssContainingText('option', item)).click();
browser.waitForAngular();

SelectBoxElement on tässä se laatikko josta valinta tehdään, ja item on merkkijono jonka sisältö tulisi löytyä valittavasta arvosta. Toimii hienosti Chromessa! (Tai toimi… Grrr…)

No Firefox ei tästä tykkää. Avaa kyllä boxin ja valintakin siirtyy oikeaan kohtaan kyllä, mutta jotenkin silti boxin suljettua vanha arvo edelleen löytyy. Syyksi paljastui bugi Selenium webdriverissa, joka ai lauo change eventtiä oikein tässä tilanteessa Firefoxissa.

No eipä hätää, Interwebin voimalla löytyi korjaus tähän:

//Open selection box
 selectBoxElement.click();
 browser.waitForAngular();

// Make selection
selectBoxElement.element(by.cssContainingText('option', item)).click();
browser.waitForAngular();

// Firefox driver fix to trigger change event for Angular
browser.actions().mouseDown().mouseUp().perform();
browser.waitForAngular();

Joka toimikin viikon. Ja nyt:

InvalidElementStateError: [Exception… ”Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsINativeMouse.click]”

Jep jep, ja tähän ei löydy patchejä oikein mistään. Yhtäkkiä siis joko webdriver tai firefox saa halvauksen tuosta simuloidusta klikkauksesta, mahdollisesti koska se on väärässä kohtaa ruutua?

Tätä saikin sitten aikansa ihmetellä. Miten syvään suohon sitä voi fanaattisella testiautomaatiolla päätyä.. Kokeilin kaikenlaista, lähettää erinäisiä loitsuja sendKeys metodilla select laatikolle, arvolle, käyttää promiseja asynkroniseen etenemiseen siltä varalta että tämä on joku ajastusongelma. Ei apua, ei toimi.

Vaan toimiipa sittenkin. Sometimes less is more. Käy ilmi että valinnan voi tehdä myös epäintuitiivisesti avaamatta selectoboxia ennen. Käy ilmi että tämä toimii Chromessa ja Firefoxissa. Käy ilmi että nykyisellään tämä on ainoa tapa tehdä valinta selectboxista onnistuneesti.

Eli tuunauksen jälkeen, tässä oman autotallin algoritmi selectboxin valintaan protractorissa:

module.exports.selectDropdownByText = function selectOption(selectBoxElement, item) {
   selectBoxElement.element(by.cssContainingText('option', item)).click();
   browser.waitForAngular();
};

Ja taas toimii.

Netbeans 8.1, Angular 1.3 ja Angular-Material

No niin, nyt on mehukas pläjäys päivityksiä. Netbeans 8.1 päivitys tipahti muutama viikko sitten, ja se paransi huomattavasti tukea Angular projektien ja Angular Seedin suhteen. Aiemmat bower pulmat ovat poistuneet – ainakin hetkeksi. Sillä pystyy tekemään hyvän Angular projektirungon siis nyt.

Angular frameworkistä vuorostaan on tullut ulos juuri tuoreeltaan odotettu 1.3 versio. Jatkossa toki luvassa vielä odotetumpi 2.0 versio joka järisyttää kaiken uusiksi. Mutta tämä 1.3 on mukava vakaa pikkupäivitys. Sen saa käyttöön vaikka heti, ja aika monet kirjastot sitä jo vaativatkin.

Lopulta angular-material. Olen kovasti itse ollut kiinnostunut Googlen Polymer/Material kirjastosta muutamasta syystä: Sitä saa niin Androidiin kuin webiinkin, se on selkeästi tablet/phablet/mobile first ui framework, se suuntaa tulevaisuuden selaimiin, ja se on aika siisti. Siinä on selkeä filosofia takana ui-kehitykseen. Ja nyt sitä saa Angulariinkin.

Kun kaikki nämä kolme lyö yksiin, tulee taas aika metka alusta kehitellä tai prototypoida. Se onnistuu esim. näin:

Lataa Netbeans 8.1 tai päivittele se tuoreimpaan versioon:

https://netbeans.org/

Tee sinne Uusi HTML 5 projekti tyyppiä Angular seed:

HTML 5 projektityyppi

Angular Seed pohja

Angular Seed antaa mukavat pohjat kehitykselle: Siellä on jo npm, ja bower mukana.

Seuraava tempaus on mennä Important Files-kansioon, editoimaan bower.json tiedostoa. Ideana on muutella siellä Angular-versiot 1.2.x versiosta versioon 1.3.x. Samalla lisää sinne myös angular-material 0.4.x, sekä angular-animate 1.3.x. Voit myös muokata projektin nimen, kuvauksen, version ja kotisivulinkin makusi mukaan. Esim- näin:

{
 "name": "Material Design Demo",
 "description": "Angular Material Design demo",
 "version": "0.0.1",
 "homepage": "https://spagettikoodi.wordpress.com",
 "license": "MIT",
 "private": true,
 "dependencies": {
 "angular": "1.3.x",
 "angular-route": "1.3.x",
 "angular-loader": "1.3.x",
 "angular-mocks": "~1.3.x",
 "angular-animate": "~1.3.x",
 "html5-boilerplate": "~4.3.0",
 "angular-material": "~0.5.x"
 },
 "resolutions": {
 "angular": "1.3.x",
 "angular-animate": "1.3.x",
 "angular-aria": "1.3.x"
 }
 
}

Huomaa tuossa myös resolutions-osa, ilman sitä saat mehukkaan angular not found errorin aikaiseksi, kun bowerille jää epäselväksi mitä versiota Angularista tulisi käytellä.

Nyt kun koko roska on valmis, aja npm install ja bower install, ja haluamasi kirjastot lataillaan interwebistä koneelle. Jos Javascriptin jumalat suovat. Taisin aiemmin kirjaillakin metkoista ongelmista npm työkalusta windows 2012 koneissa suhteessa npm central registryyn. Tätä kirjoittaessa ne ovat edelleen olemassa, täältä suomen kamaralta suunnattuna.

Npm and Bower run tools

Angular-Material kirjasto vaatii index-sivulle muutaman linkin toimiakseen: tarvitset linkit javascript-tiedostoihin ja tyylisivuihin. Ne kannattaa laittaa olemassaolevien kaveriksi, ja huomioida myös miten aiempien linkkien polut bower-components kansioon toimivat. Mutta tähän tapaan:

<!DOCTYPE html>
<!--[if lt IE 7]> <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html lang="en" ng-app="myApp" class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html lang="en" ng-app="myApp" class="no-js"> <!--<![endif]-->
 <head>
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <title>My AngularJS App</title>
 <meta name="description" content="">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
 <link rel="stylesheet" href="/bower_components/angular-material/angular-material.css">
 <link rel="stylesheet" href="/bower_components/angular-material/themes/default-theme.css">
 <link rel="stylesheet" href="bower_components/html5-boilerplate/css/normalize.css">
 <link rel="stylesheet" href="bower_components/html5-boilerplate/css/main.css">
 <link rel="stylesheet" href="app.css">
 <script src="bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js"></script>
 </head>
 <body>
 <ul class="menu">
 <li><a href="#/view1">view1</a></li>
 <li><a href="#/view2">view2</a></li>
 </ul>

<!--[if lt IE 9]>

 <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
 <![endif]-->
<div ng-view></div>
<div>Angular seed app: v<span app-version></span></div>
<!-- In production use:
 <script src="//ajax.googleapis.com/ajax/libs/angularjs/x.x.x/angular.min.js"></script>
 -->
 <script src="bower_components/angular/angular.js"></script>
 <script src="/bower_components/angular-aria/angular-aria.js"></script>
 <script src="/bower_components/angular-animate/angular-animate.js"></script>
 <script src="/bower_components/hammerjs/hammer.js"></script>
 <script src="/bower_components/angular-material/angular-material.js"></script> 
 <script src="bower_components/angular-route/angular-route.js"></script>
 <script src="app.js"></script>
 <script src="view1/view1.js"></script>
 <script src="view2/view2.js"></script>
 <script src="components/version/version.js"></script>
 <script src="components/version/version-directive.js"></script>
 <script src="components/version/interpolate-filter.js"></script>
 </body>
</html>

Editoi app.js tiedostoa, lisää sinne material design moduuli niin saat myös funktiot esim. modaali-ikkunoille käyttöön:

’use strict’;

// Declare app level module which depends on views, and components
angular.module('myApp', [
 'ngRoute',
 'ngMaterial',
 'myApp.view1',
 'myApp.view2',
 'myApp.version'
]).
config(['$routeProvider', function($routeProvider) {
 $routeProvider.otherwise({redirectTo: '/view1'});
}]);

Lopuksi, jos haluat heti nähdä toimivatko linkit suht oikein, avaa view1/view1.html tiedosto, ja lisää sinne material-design nappi.

<p>This is the partial for view 1.</p>
<md-button class="md-fab md-raised">PUSH ME</md-button>

Huomaa pari seikkaa:

– Paketissa tulee mukana myös angular-aria ja hammerjs, nekin mielenkiintoisia kirjastoja tabsujen ja kämmyköiden kanssa.

– Näissä linkeissä on viittaus angular/material kansion alla olevaan themes/default-theme.css tiedostoon, se tulee angular-material 0.5:sta alkaen mukana.

Jos haluat, voit nyt ajaa sovelluksen ja tarkistaa että virheilmoituksia ei tule ja nappi tulee näkyviin. Se tapahtuu Netbeansissä yksinkertaisesti oikealla hiirennapilla projektin päällä:

Run app

 

First material design button

 

Huomaa material design napin onhover efekti ja onclick ripple-efekti. Siinä esimakua mitä on luvassa.

No niin, se siitä. Nyt sinulla on Netbeans 8.1, AngularJS 1.3, sekä angular-material asennettuna, ja hupi voi alkaa. Mitäpä tällä kombolla voi tehdä? Kirjoitan siitä toisen artikkelin myöhemmin.

Tässä muutama asiaan liittyvä linkkivinkki:

https://github.com/angular/angular.js/blob/master/CHANGELOG.md

https://material.angularjs.org/#/

https://github.com/angular/bower-material