Tuesday, August 4, 2015

Custom elements for Chrome Apps APIs

Continuing with my Polymer Chrome App I needed access to the Chrome Storage API. A quick search revealed only elements that haven't been updated to Polymer 1.0 so I started to create my own element based on iron-localstorage.

The "problem" with elements that depend on Chrome Apps APIs is that you can't test/use them outside of Chrome Apps, so I went ahead and created some gulp tasks to make things easier for me.

The main idea of these tasks is to put the contents of demo or test, which you would normally run directly, and all dependencies into one Chrome app that would then use demo/index.html or test/index.html as main page.

First I take all the files relevant for the element itself + the test/demo files, run the html files through crisper and put the result into the output components/my-element/ folder (following the layout of gh-pages for Polymer elements).
// Get folder name (that should match the name of the element)
var element = __dirname.split(path.sep).pop();
// All files necessary for the element + test files
// This will have to be extended, e.g. if your element uses images
gulp.src([
'./*.html',
'./*.js',
'./*.css',
'./test/**',
'!./gulpfile.js',
'!./index.html',
],{base: './'})
// run all html files through crisper for CSP
.pipe($.if('*.html', $.crisper()))
// put everything into ./{{dest}}/components/{your-element}/
.pipe(gulp.dest(path.join(destDir, 'components', element)));
view raw gulp-copy.js hosted with ❤ by GitHub
All bower dependencies are run through crisper as well and put into the components folder.
// take all bower dependencies
gulp.src(['bower_components/**/*'])
// run all html files through crisper for CSP
.pipe($.if('*.html', $.crisper()))
// put everything into ./{{dest}}/components/
.pipe(gulp.dest(path.join(destDir, 'components')));
view raw gulp-bower.js hosted with ❤ by GitHub
For the Chrome App itself only two files are important.

manifest.json defines the necessary permissions (e.g. to use chrome-storage the "storage" permission is required).
{
"manifest_version": 2,
"name": "_element_ _type_",
"version": "0.0.1",
"minimum_chrome_version": "34",
"app": {
"background": {
"scripts": ["main.js"]
}
},
"permissions": [
"storage"
]
}
view raw manifest.json hosted with ❤ by GitHub
main.js launches the test or demo page.
/**
* Listens for the app launching then creates the window
*/
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create('components/_element_/_type_/index.html');
});
view raw main.js hosted with ❤ by GitHub
The gulp task copies those two files to the main output folder, and changes the window.create call to point to the right file, e.g at components/my-element/test/index.html
gulp.src(['./chrome-app/**'])
.pipe($.replace('_element_', element))
.pipe($.replace('_type_', 'test'))
.pipe(gulp.dest(destDir));
The Chrome demo and test apps created that way can then be loaded as unpacked extensions.



This works nicely for the demo app, but unfortunately the test app reveals this in the console when starting it:

Uncaught Error: document.write() is not available in packaged apps.

Investigating the problem reveals this line in the web-component-tester to cause the issue, which makes sure that all dependencies are loaded before WCT is actually started.

To work around this issue you have to include the necessary scripts in the test files explicitly...
<script src="../../stacky/lib/parsing.js"></script>
<script src="../../stacky/lib/formatting.js"></script>
<script src="../../stacky/lib/normalization.js"></script>
<script src="../../async/lib/async.js"></script>
<script src="../../lodash/lodash.js"></script>
<script src="../../mocha/mocha.js"></script>
<script src="../../chai/chai.js"></script>
<script src="../../sinonjs/sinon.js"></script>
<script src="../../sinon-chai/lib/sinon-chai.js"></script>
<script src="../../accessibility-developer-tools/dist/js/axs_testing.js"></script>
...and tell it not to load any scripts itself...
WCT = {
environmentScripts: []
};
view raw prepare_wct.js hosted with ❤ by GitHub
...before loading web-component-tester/browser.js on all the test pages.

So that I don't have to copy the same couple of lines into each of the test files separately, I extended the gulp-copy task to automatically insert the necessary lines in all files that include a reference to web-component-tester/browser.js
var wctScripts = [
'<script src="../../stacky/lib/parsing.js"></script>',
'<script src="../../stacky/lib/formatting.js"></script>',
'<script src="../../stacky/lib/normalization.js"></script>',
'<script src="../../async/lib/async.js"></script>',
'<script src="../../lodash/lodash.js"></script>',
'<script src="../../mocha/mocha.js"></script>',
'<script src="../../chai/chai.js"></script>',
'<script src="../../sinonjs/sinon.js"></script>',
'<script src="../../sinon-chai/lib/sinon-chai.js"></script>',
'<script src="../../accessibility-developer-tools/dist/js/axs_testing.js"></script>',
'<script src="prepare_wct.js"></script>'
].join('\n');
gulp.src(...)
...
// Insert WCT Scripts in test files before WCT is loaded
.pipe(
$.if('*.html', $.insertLines({
before: /<script\ src="..\/..\/web-component-tester\/browser.js/,
lineBefore: wctScripts
}))
)
view raw gulp-wct.js hosted with ❤ by GitHub
And with this change tests can be run in the Chrome App.


Following the idea of my previous article I also wanted to enable live-reload for this workflow

As opposed to my article where the livereload.js is removed for the production build, I did it the other way round here by adding it to the test/index.html or demo/index.html when running the gulp-live-task.
// Insert live-reload script into main demo/test files
.pipe(
$.if(
'**/index.html',
$.insertLines({
before: /<\/head>/,
lineBefore: '<script src="../../chrome-app-livereload/livereload.js' +
'?host=localhost&amp;port=35729"></script>'
})
)
)
Watching for changes, rebuilding the app if necessary and triggering the reload works basically the same though.
var lr = tinylr();
lr.listen(35729);
// watch for changes and rebuild the chrome app if necessary
gulp.watch(['./*', './test/**'], ['copy-live:test']);
gulp.watch(['./chrome-app/**'], ['app:test']);
gulp.watch(['bower_components/**'], ['bower:test']);
// trigger live-reload when the app was changed in some way
gulp.watch([destDir + '/**'], $.batch({timeout: 500}, function (events, cb) {
var paths = [];
events.on('data', function (evt) {
paths.push(evt.path);
}).on('end', function () {
lr.changed({
body: {files: paths}
});
cb();
});
}));
view raw gulp-watch.js hosted with ❤ by GitHub
And with this I can leave the test and/or demo apps running and see right away if all tests still pass after making changes and if the demos work as expected.

And now back to actually working on my app. All those distractions you run into while traversing (mostly) uncharted waters ☺