feed your grails karma (#ggx 2014)
TRANSCRIPT
![Page 1: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/1.jpg)
Feed Your Grails KarmaVladimír Oraný
![Page 2: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/2.jpg)
Overview
Test Driven Backend REST Application (Grails, Spock)
Test Driven Frontend Single Page Application (AngularJS, Jasmine,
Karma, Geb)
Continuous Integration capable Test Pipeline (Travis CI)
![Page 3: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/3.jpg)
Grails
Rapid web development framework
Plays well with Test Driven Development …
… as long as you stick with GSPs
Great REST support since 2.3
![Page 4: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/4.jpg)
Geb
Browser automation solution
jQuery-like selectors
plays well with Grails and Spock
great tool for functional tests
![Page 5: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/5.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 6: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/6.jpg)
Model Catalogue Plugin
Grails PLugin for managing any
metadata
REST API Backend
AngularJS services and directives
https://github.com/MetadataRegistry/
ModelCataloguePlugin
![Page 7: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/7.jpg)
“A single-page application (SPA), is a web application or web site
that fits on a single web page with the goal of providing a more
fluid user experience akin to a desktop application.”
–The Poeple of the Internet (aka Wikipedia)
Single Page Application
![Page 8: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/8.jpg)
Ancient Architecture
ServerFat
Client
Shared Libararies
![Page 9: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/9.jpg)
SPA Architecture
REST
backend
Rich
Client
REST API (JSON)
SPA
![Page 10: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/10.jpg)
Sample Application(s)
![Page 11: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/11.jpg)
![Page 12: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/12.jpg)
Earl’s List
Backend application in Grails
Frontend application in AngularJS
with little help of Grails (Asset Pipeline)
https://github.com/musketyr/earls-list
![Page 13: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/13.jpg)
Backend Application
Grails REST application
grails create-app earls-list
cd earls-list
grails create-domain-class org.example.todo.Item
grails create-controller org.example.todo.Item
![Page 14: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/14.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 15: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/15.jpg)
@Unroll void “item from #params has #errorCount errors"() {when:Item item = new Item(params).save(failOnError: true)
then:item.errors.errorCount == errorCount
where:errorCount | params1 | [description: '']0 | [description: 'Do IT']1 | [description: 'x' * 256]
}
![Page 16: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/16.jpg)
class Item {String descriptionBoolean crossed = Boolean.FALSE
static constraints = {description size: 1..255
}}
![Page 17: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/17.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 18: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/18.jpg)
class ItemControllerSpec extends IntegrationSpec {
void "create new item"() {when:
controller.response.format = 'json' controller.request.json =[description: 'Do IT!']controller.request.method = 'POST'controller.save()
def result = controller.response.json
then:result.description == 'Do IT'
}
![Page 19: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/19.jpg)
class ItemController extends RestfulController<Item> {
def responseFormats = ['json']
ItemController() {super(Item)
}
}
![Page 20: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/20.jpg)
@TestFor(UrlMappings) @Mock(ItemController)class UrlMappingSpec extends Specification {
void "test item rest endpoints ready"() {expect:assertRestForwardUrlMapping(method, url,
controller: "item", action: action, paramsToCheck)
where:method | action | url | paramsToCheck"GET" | "index" | "/item/" | {}"POST" | "save" | "/item/" | {}"PUT" | "update" | "/item/1" | { id = "1" }"DELETE" | "delete" | "/item/1" | { id = "1" }
}
![Page 21: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/21.jpg)
class UrlMappings {
static mappings = {"/item/" controller: 'item', action: 'index',
method: HttpMethod.GET"/item/" controller: 'item', action: 'save',
method: HttpMethod.POST"/item/$id" controller: 'item', action: 'update',
method: HttpMethod.PUT"/item/$id" controller: 'item', action: ‘delete',
method: HttpMethod.DELETE}
}
![Page 22: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/22.jpg)
![Page 23: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/23.jpg)
Frontend Application
AngularJS for controllers, services and templates
dependency injection => easier testing
Bootstrap for theming
Jasmine and Karma for testing
![Page 24: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/24.jpg)
SPA Architecture
REST
backend
Rich
Client
REST API (JSON)
![Page 25: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/25.jpg)
JsonRecorder
Records the JSON i/o in the controller tests
repositories {mavenRepo "http://dl.bintray.com/metadata/model-catalogue"
}
dependencies {test "org.modelcatalogue:json-recorder:0.1.0"
}
![Page 26: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/26.jpg)
JsonRecorder
def rec = JsonRecorder.create('test/js-fixtures/earlslist', 'item')
void "obtain the list of items in json format"() {when:controller.index(10)
def result = rec << 'list' << controller.response.json
then:result?.size() == 10result[0].description =~ /Item #\d+/
}
![Page 27: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/27.jpg)
(function (window) {window['fixtures'] = window['fixtures'] || {};var fixtures = window['fixtures'];fixtures['item'] = fixtures['item'] || {};var item = fixtures['item'];window.fixtures.item.list = [
{"id": 17,"description": "Item #16","class": "org.example.todo.Item","crossed": false,"dateCreated": "2014-12-07T04:56:44Z"
},...
];})(window);
![Page 28: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/28.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 29: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/29.jpg)
Where to find frontend tools and libraries?
![Page 30: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/30.jpg)
![Page 31: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/31.jpg)
![Page 32: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/32.jpg)
![Page 33: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/33.jpg)
![Page 34: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/34.jpg)
![Page 35: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/35.jpg)
This slide was intentionally left blank …
![Page 36: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/36.jpg)
Front-end Developer Toolbox
![Page 37: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/37.jpg)
NodeJS
Platform for creating server application in JavaScript
Platform for creating/sharing tools for frontend developers
Package manager NPM
npm install
Plays well with CI servers
![Page 38: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/38.jpg)
package.json
{"name": "earls-list","description": "Sample TODO List","version": "0.0.1","devDependencies": {
"bower": "~1.3.12","karma": "~0.12.0","karma-junit-reporter": "~0.2.1","karma-jasmine": "~0.2.0","karma-firefox-launcher": "~0.1.3"
}}
![Page 39: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/39.jpg)
Bower
Package manager for frontend application
Based on Git
bower.json with similar syntax as package.json (NPM)
bower install
![Page 40: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/40.jpg)
bower.json
{"name": "earls-list","description": "Sample TODO List","version": "0.0.1","dependencies": {"angular": "~1.3.5","bootstrap": "~3.3.1"
},"devDependencies": {"jasmine": "~2.1.3","angular-mocks": "~1.3.5"
}}
![Page 41: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/41.jpg)
.bowerrc
{"directory": "grails-app/assets/bower_components"
}
![Page 42: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/42.jpg)
Asset Pipeline Plugin
//= require jquery/dist/jquery//= require angular/angular//= require bootstrap/dist/js/bootstrap
//= require items//= require itemsCtrl
angular.module('earlslist', ['earlslist.items','earlslist.itemsCtrl'
]);
![Page 43: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/43.jpg)
Asset Pipeline Plugin
<!DOCTYPE html><html><head><title>Earl's List</title><asset:stylesheet src="earlslist/items.css"/><asset:javascript src="earlslist/index.js"/><script type="text/javascript">angular.module('earlslist.apiRoot', []).value('apiRoot', '${request.contextPath ?: ''}');
</script></head>
![Page 44: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/44.jpg)
Jasmine
Behaviour-Driven JavaScript
Assertions with matchers
Mocking and Spying
![Page 45: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/45.jpg)
describe('Items service wraps the backend', function(){// setupbeforeEach(module('earlslist.items'));
it('should list all items', inject(function(…){// feature method
}));
...}
![Page 46: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/46.jpg)
it('should list all items', inject(function(items, $httpBackend){var list = null;
$httpBackend.expectGET(‘/item/‘).respond(angular.copy(fixtures.item.list));
items.list().then(function(fetched){ list = fetched; });
expect(list).toBeNull();
$httpBackend.flush();
expect(list).toBeDefined();expect(list.length).toBe(10);
}));
![Page 47: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/47.jpg)
it('should create new item', inject(function(items, $httpBackend){var item = null;
$httpBackend.expectPOST('/item/', fixtures.item.validSaveInput).respond(angular.copy(fixtures.item.validSave));
items.save(fixtures.item.validSaveInput).then(function(created){item = created;
});
expect(item).toBeNull();
$httpBackend.flush();
expect(item).toBeDefined();expect(item.id).toBe(35);
}));
![Page 48: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/48.jpg)
angular.module(‘earlslist.items', function($http, apiRoot){
var items, unwrapOrReject, enhanceAll, enhanceItem;
unwrapOrReject = function(response){…};
enhanceItem = function(item) {…};
enhanceAll = function(listOfItems) {…};
return {
list: function(start) {
return $http({
url: apiRoot + "/item/",
method: 'GET',
params: start ? {offset: start } : {}
}).then(unwrapOrReject).then(enhanceAll);
},
...
};
});
![Page 49: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/49.jpg)
Karma
Test runner for JavaScript tests
Can run multiple browsers including HtmlUnit and PhantomJS
karma start karma.conf.json
![Page 50: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/50.jpg)
module.exports = function(config) {config.set({
...files: [
'grails-app/assets/bower_components/jquery/dist/jquery.js','grails-app/assets/bower_components/angular/angular.js','grails-app/assets/javascripts/**/*.js','grails-app/assets/bower_components/angular-mocks/angular-mocks.js','test/js-fixtures/**/*.js','test/js/**/*.js'
],...
});};
![Page 51: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/51.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 52: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/52.jpg)
<body ng-app="earlslist"><div class="container"><div ng-include="'earlslist/items.html'"></div>
</div></body>
![Page 53: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/53.jpg)
ctrModule.run([‘$templateCache', function($templateCache){
$templateCache.put('earlslist/items.html',
'<div class="row" ng-controller="earlslist.itemsCtrl">' +
...
'</div>');
}]);
![Page 54: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/54.jpg)
<form ng-submit="addItem()" class="col-md-12 form"><div class="alert alert-danger" ng-repeat="error in errors">{{error.message}}
</div><input ng-model="newItemText" ng-disabled="loading">
</form><h2 ng-repeat="item in items"><span ng-click="toggle(item)"></span><span class=“item-description" ng-click=“toggle(item)”>
{{item.description}}</span><span class="pull-right" ng-click="remove(item)">Delete</span>
</h2>
![Page 55: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/55.jpg)
angular.module('earlslist.itemsCtrl', ['earlslist.items']).controller('earlslist.itemsCtrl', function($scope, items){...$scope.addItem = function() {
items.save({description: $scope.newItemText}).then(function(newItem){
$scope.newItemText = '';$scope.items.unshift(newItem);$scope.errors = [];
}).catch(function(response){$scope.errors = response.data.errors;
});};
});
![Page 56: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/56.jpg)
beforeEach(inject(function ($rootScope, $controller, _items_, $q) {items = _items_;$scope = $rootScope.$new();
// returns 10 items for first call and 3 for secondspyOn(items, 'list').and.callFake(function () {…});
$controller('earlslist.itemsCtrl', {$scope: $scope, items: items
});}));
![Page 57: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/57.jpg)
it('should list all items', function(){expect($scope.errors.length).toBe(0);expect($scope.items.length).toBe(0);expect($scope.loading).toBe(true);
$scope.$digest();
expect($scope.errors.length).toBe(0);expect($scope.items.length).toBe(13);expect($scope.loading).toBe(false);
});
![Page 58: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/58.jpg)
it('renders 13 items', inject(function($templateCache, $compile) {var tpl, element;
tpl = $compile($templateCache.get(‘earlslist/items.html'))element = tpl($scope);
$scope.$digest();
expect(element.find('.item').length).toBe(13);
}));
![Page 59: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/59.jpg)
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
![Page 60: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/60.jpg)
![Page 61: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/61.jpg)
Functional tests with Geb
reportsDir = new File("target/geb-reports")reportOnTestFailureOnly = falsebaseUrl = 'http://localhost:8080/earls-list/'
driver = { new FirefoxDriver() }
waiting {timeout = 15retryInterval = 0.6
}
// Default to wraping `at SomePage` declarations in `waitFor` closuresatCheckWaiting = true
![Page 62: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/62.jpg)
class HomePage extends Page {static url = "#/"static at = { heading.text() == "Earl's List" }static content = {
heading { $("h1") }task { $("#task") }items(required: false) { $(".item") }itemsTexts(required: false) {
items.find('.item-description') }errors(required: false) { $(".alert-danger") }closeError(required: false) { $(".close") }
}}
![Page 63: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/63.jpg)
def "go to home page and enter new task"() {when:
to HomePagethen:
at HomePageexpect:
!items.size()when:
task = 'Test with Geb'task << Keys.ENTER
then:waitFor {
items.size() == 1}
}
![Page 64: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/64.jpg)
![Page 65: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/65.jpg)
„Will it play on continuous integration server?“
![Page 66: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/66.jpg)
Travis CI
free for public GitHub repostories
configuration stored in the repository
NodeJS available for every build
Firefox and Chrome for headless testing
![Page 67: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/67.jpg)
.travis.yml
language: groovyjdk:- oraclejdk7before_install:- "npm install"- "bower install"- "export DISPLAY=:99.0"- "sh -e /etc/init.d/xvfb start"
script:- "./grailsw refresh-dependencies"- "./grailsw test-app"- "./node_modules/karma/bin/karma start --single-run --browsers Firefox"
![Page 68: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/68.jpg)
![Page 69: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/69.jpg)
Summary
Grails SPAs are actually two applications
Use the front-end developers tools for front-end app
Use your backend tests to generate the fixtures
https://github.com/musketyr/earls-list
![Page 70: Feed Your Grails Karma (#ggx 2014)](https://reader038.vdocument.in/reader038/viewer/2022110310/55a63da61a28ab70778b4706/html5/thumbnails/70.jpg)
Thank you
Vladimír Oraný
@musketyr