Cleanup and upgrade unit tests environment

`karma.conf.ci.js` has been merged into `karma.conf.js` for local testing consistency: `gulp unittestWatch` has been replaced by `gulp unittest --watch` and thus use exactly the same config file. Upgrade to latest jasmine and karma packages and remove deprecated `gulp-karma` dependency (directly use `karma.Server` in gulp).

Split `test/mockContext.js` into smaller `test/jasmine.*` modules to make easier unit tests maintenance and finally, move all `*.test.js` files under the `test/specs` folder.
This commit is contained in:
Simon Brunel
2017-03-04 16:47:53 +01:00
committed by Evert Timberg
parent b4dfa38731
commit c216c0af76
39 changed files with 431 additions and 431 deletions

View File

@@ -12,12 +12,13 @@ var uglify = require('gulp-uglify');
var util = require('gulp-util');
var zip = require('gulp-zip');
var exec = require('child_process').exec;
var karma = require('gulp-karma');
var karma = require('karma');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var merge = require('merge-stream');
var collapse = require('bundle-collapser/plugin');
var argv = require('yargs').argv
var path = require('path');
var package = require('./package.json');
var srcDir = './src/';
@@ -34,14 +35,6 @@ var header = "/*!\n" +
" * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" +
" */\n";
var preTestFiles = [
'./node_modules/moment/min/moment.min.js'
];
var testFiles = [
'./test/*.js'
];
gulp.task('bower', bowerTask);
gulp.task('build', buildTask);
gulp.task('package', packageTask);
@@ -53,7 +46,6 @@ gulp.task('size', ['library-size', 'module-sizes']);
gulp.task('server', serverTask);
gulp.task('validHTML', validHTMLTask);
gulp.task('unittest', unittestTask);
gulp.task('unittestWatch', unittestWatchTask);
gulp.task('library-size', librarySizeTask);
gulp.task('module-sizes', moduleSizesTask);
gulp.task('_open', _openTask);
@@ -157,6 +149,7 @@ function lintTask() {
'beforeEach',
'describe',
'expect',
'fail',
'it',
'jasmine',
'moment',
@@ -177,37 +170,31 @@ function validHTMLTask() {
}
function startTest() {
return [].concat(preTestFiles).concat([
'./src/**/*.js',
'./test/mockContext.js'
]).concat(
argv.inputs?
argv.inputs.split(';'):
testFiles);
return [
'./node_modules/moment/min/moment.min.js',
'./test/jasmine.index.js',
'./src/**/*.js',
].concat(
argv.inputs?
argv.inputs.split(';'):
['./test/specs/**/*.js']
);
}
function unittestTask() {
return gulp.src(startTest())
.pipe(karma({
configFile: 'karma.conf.ci.js',
action: 'run'
}));
function unittestTask(done) {
new karma.Server({
configFile: path.join(__dirname, 'karma.conf.js'),
singleRun: !argv.watch,
files: startTest(),
}, done).start();
}
function unittestWatchTask() {
return gulp.src(startTest())
.pipe(karma({
configFile: 'karma.conf.js',
action: 'watch'
}));
}
function coverageTask() {
return gulp.src(startTest())
.pipe(karma({
configFile: 'karma.coverage.conf.js',
action: 'run'
}));
function coverageTask(done) {
new karma.Server({
configFile: path.join(__dirname, 'karma.coverage.conf.js'),
files: startTest(),
singleRun: true,
}, done).start();
}
function librarySizeTask() {

View File

@@ -1,25 +0,0 @@
module.exports = function(config) {
var configuration = {
browsers: ['Firefox'],
customLaunchers: {
Chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},
frameworks: ['browserify', 'jasmine'],
reporters: ['progress', 'html'],
preprocessors: {
'src/**/*.js': ['browserify']
},
browserify: {
debug: true
}
};
if (process.env.TRAVIS) {
configuration.browsers.push('Chrome_travis_ci');
}
config.set(configuration);
};

View File

@@ -1,14 +1,33 @@
module.exports = function(config) {
config.set({
browsers: ['Chrome', 'Firefox'],
/* eslint camelcase: 0 */
module.exports = function(karma) {
var config = {
browsers: ['Firefox'],
frameworks: ['browserify', 'jasmine'],
reporters: ['progress', 'html'],
reporters: ['progress', 'kjhtml'],
preprocessors: {
'src/**/*.js': ['browserify']
'./test/jasmine.index.js': ['browserify'],
'./src/**/*.js': ['browserify']
},
browserify: {
debug: true
}
});
};
};
// https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647
if (process.env.TRAVIS) {
config.browsers.push('chrome_travis_ci');
config.customLaunchers = {
chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
};
} else {
config.browsers.push('Chrome');
}
karma.set(config);
};

View File

@@ -1,12 +1,14 @@
module.exports = function(config) {
var configuration = {
browsers: ['Firefox'],
frameworks: ['browserify', 'jasmine'],
reporters: ['progress', 'coverage'],
preprocessors: {
'src/**/*.js': ['browserify']
'./test/jasmine.index.js': ['browserify'],
'./src/**/*.js': ['browserify']
},
browserify: {
debug: true,
transform: [['browserify-istanbul', {
@@ -15,8 +17,7 @@ module.exports = function(config) {
}
}]]
},
reporters: ['progress', 'coverage'],
coverageReporter: {
dir: 'coverage/',
reporters: [

View File

@@ -21,22 +21,21 @@
"gulp-file": "^0.3.0",
"gulp-html-validator": "^0.0.2",
"gulp-insert": "~0.5.0",
"gulp-karma": "0.0.4",
"gulp-replace": "^0.5.4",
"gulp-size": "~0.4.0",
"gulp-streamify": "^1.0.2",
"gulp-uglify": "~2.0.x",
"gulp-util": "~2.2.x",
"gulp-zip": "~3.2.0",
"jasmine": "^2.3.2",
"jasmine-core": "^2.3.4",
"karma": "^0.12.37",
"karma-browserify": "^5.0.1",
"karma-chrome-launcher": "^0.2.0",
"karma-coverage": "^0.5.1",
"karma-firefox-launcher": "^0.1.6",
"karma-jasmine": "^0.3.6",
"karma-jasmine-html-reporter": "^0.1.8",
"jasmine": "^2.5.0",
"jasmine-core": "^2.5.0",
"karma": "^1.5.0",
"karma-browserify": "^5.1.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.0",
"karma-firefox-launcher": "^1.0.0",
"karma-jasmine": "^1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"merge-stream": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0",

125
test/jasmine.context.js Normal file
View File

@@ -0,0 +1,125 @@
// Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing
var Context = function() {
this._calls = []; // names/args of recorded calls
this._initMethods();
this._fillStyle = null;
this._lineCap = null;
this._lineDashOffset = null;
this._lineJoin = null;
this._lineWidth = null;
this._strokeStyle = null;
// Define properties here so that we can record each time they are set
Object.defineProperties(this, {
fillStyle: {
get: function() {
return this._fillStyle;
},
set: function(style) {
this._fillStyle = style;
this.record('setFillStyle', [style]);
}
},
lineCap: {
get: function() {
return this._lineCap;
},
set: function(cap) {
this._lineCap = cap;
this.record('setLineCap', [cap]);
}
},
lineDashOffset: {
get: function() {
return this._lineDashOffset;
},
set: function(offset) {
this._lineDashOffset = offset;
this.record('setLineDashOffset', [offset]);
}
},
lineJoin: {
get: function() {
return this._lineJoin;
},
set: function(join) {
this._lineJoin = join;
this.record('setLineJoin', [join]);
}
},
lineWidth: {
get: function() {
return this._lineWidth;
},
set: function(width) {
this._lineWidth = width;
this.record('setLineWidth', [width]);
}
},
strokeStyle: {
get: function() {
return this._strokeStyle;
},
set: function(style) {
this._strokeStyle = style;
this.record('setStrokeStyle', [style]);
}
},
});
};
Context.prototype._initMethods = function() {
// define methods to test here
// no way to introspect so we have to do some extra work :(
var me = this;
var methods = {
arc: function() {},
beginPath: function() {},
bezierCurveTo: function() {},
clearRect: function() {},
closePath: function() {},
fill: function() {},
fillRect: function() {},
fillText: function() {},
lineTo: function() {},
measureText: function(text) {
// return the number of characters * fixed size
return text ? {width: text.length * 10} : {width: 0};
},
moveTo: function() {},
quadraticCurveTo: function() {},
restore: function() {},
rotate: function() {},
save: function() {},
setLineDash: function() {},
stroke: function() {},
strokeRect: function() {},
setTransform: function() {},
translate: function() {},
};
Object.keys(methods).forEach(function(name) {
me[name] = function() {
me.record(name, arguments);
return methods[name].apply(me, arguments);
};
});
};
Context.prototype.record = function(methodName, args) {
this._calls.push({
name: methodName,
args: Array.prototype.slice.call(args)
});
};
Context.prototype.getCalls = function() {
return this._calls;
};
Context.prototype.resetCalls = function() {
this._calls = [];
};
module.exports = Context;

53
test/jasmine.index.js Normal file
View File

@@ -0,0 +1,53 @@
var Context = require('./jasmine.context');
var matchers = require('./jasmine.matchers');
var utils = require('./jasmine.utils');
(function() {
// Keep track of all acquired charts to automatically release them after each specs
var charts = {};
function acquireChart() {
var chart = utils.acquireChart.apply(utils, arguments);
charts[chart.id] = chart;
return chart;
}
function releaseChart(chart) {
utils.releaseChart.apply(utils, arguments);
delete charts[chart.id];
}
function createMockContext() {
return new Context();
}
window.acquireChart = acquireChart;
window.releaseChart = releaseChart;
window.createMockContext = createMockContext;
// some style initialization to limit differences between browsers across different plateforms.
utils.injectCSS(
'.chartjs-wrapper, .chartjs-wrapper canvas {' +
'border: 0;' +
'margin: 0;' +
'padding: 0;' +
'}' +
'.chartjs-wrapper {' +
'position: absolute' +
'}');
beforeEach(function() {
jasmine.addMatchers(matchers);
});
afterEach(function() {
// Auto releasing acquired charts
Object.keys(charts).forEach(function(id) {
var chart = charts[id];
if (!(chart.$test || {}).persistent) {
releaseChart(chart);
}
});
});
}());

113
test/jasmine.matchers.js Normal file
View File

@@ -0,0 +1,113 @@
'use strict';
function toBeCloseToPixel() {
return {
compare: function(actual, expected) {
var result = false;
if (!isNaN(actual) && !isNaN(expected)) {
var diff = Math.abs(actual - expected);
var A = Math.abs(actual);
var B = Math.abs(expected);
var percentDiff = 0.005; // 0.5% diff
result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
}
return {pass: result};
}
};
}
function toEqualOneOf() {
return {
compare: function(actual, expecteds) {
var result = false;
for (var i = 0, l = expecteds.length; i < l; i++) {
if (actual === expecteds[i]) {
result = true;
break;
}
}
return {
pass: result
};
}
};
}
function toBeValidChart() {
return {
compare: function(actual) {
var message = null;
if (!(actual instanceof Chart)) {
message = 'Expected ' + actual + ' to be an instance of Chart';
} else if (!(actual.canvas instanceof HTMLCanvasElement)) {
message = 'Expected canvas to be an instance of HTMLCanvasElement';
} else if (!(actual.ctx instanceof CanvasRenderingContext2D)) {
message = 'Expected context to be an instance of CanvasRenderingContext2D';
} else if (typeof actual.height !== 'number' || !isFinite(actual.height)) {
message = 'Expected height to be a strict finite number';
} else if (typeof actual.width !== 'number' || !isFinite(actual.width)) {
message = 'Expected width to be a strict finite number';
}
return {
message: message? message : 'Expected ' + actual + ' to be valid chart',
pass: !message
};
}
};
}
function toBeChartOfSize() {
return {
compare: function(actual, expected) {
var res = toBeValidChart().compare(actual);
if (!res.pass) {
return res;
}
var message = null;
var canvas = actual.ctx.canvas;
var style = getComputedStyle(canvas);
var pixelRatio = window.devicePixelRatio;
var dh = parseInt(style.height, 10);
var dw = parseInt(style.width, 10);
var rh = canvas.height;
var rw = canvas.width;
var orh = rh / pixelRatio;
var orw = rw / pixelRatio;
// sanity checks
if (actual.height !== orh) {
message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh;
} else if (actual.width !== orw) {
message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw;
}
// validity checks
if (dh !== expected.dh) {
message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
} else if (dw !== expected.dw) {
message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
} else if (rh !== expected.rh) {
message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
} else if (rw !== expected.rw) {
message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
}
return {
message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
pass: !message
};
}
};
}
module.exports = {
toBeCloseToPixel: toBeCloseToPixel,
toEqualOneOf: toEqualOneOf,
toBeValidChart: toBeValidChart,
toBeChartOfSize: toBeChartOfSize
};

76
test/jasmine.utils.js Normal file
View File

@@ -0,0 +1,76 @@
/**
* Injects a new canvas (and div wrapper) and creates teh associated Chart instance
* using the given config. Additional options allow tweaking elements generation.
* @param {object} config - Chart config.
* @param {object} options - Chart acquisition options.
* @param {object} options.canvas - Canvas attributes.
* @param {object} options.wrapper - Canvas wrapper attributes.
* @param {boolean} options.persistent - If true, the chart will not be released after the spec.
*/
function acquireChart(config, options) {
var wrapper = document.createElement('div');
var canvas = document.createElement('canvas');
var chart, key;
config = config || {};
options = options || {};
options.canvas = options.canvas || {height: 512, width: 512};
options.wrapper = options.wrapper || {class: 'chartjs-wrapper'};
for (key in options.canvas) {
if (options.canvas.hasOwnProperty(key)) {
canvas.setAttribute(key, options.canvas[key]);
}
}
for (key in options.wrapper) {
if (options.wrapper.hasOwnProperty(key)) {
wrapper.setAttribute(key, options.wrapper[key]);
}
}
// by default, remove chart animation and auto resize
config.options = config.options || {};
config.options.animation = config.options.animation === undefined? false : config.options.animation;
config.options.responsive = config.options.responsive === undefined? false : config.options.responsive;
config.options.defaultFontFamily = config.options.defaultFontFamily || 'Arial';
wrapper.appendChild(canvas);
window.document.body.appendChild(wrapper);
chart = new Chart(canvas.getContext('2d'), config);
chart.$test = {
persistent: options.persistent,
wrapper: wrapper
};
return chart;
}
function releaseChart(chart) {
chart.destroy();
var wrapper = (chart.$test || {}).wrapper;
if (wrapper && wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);
}
}
function injectCSS(css) {
// http://stackoverflow.com/q/3922139
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.setAttribute('type', 'text/css');
if (style.styleSheet) { // IE
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
module.exports = {
injectCSS: injectCSS,
acquireChart: acquireChart,
releaseChart: releaseChart
};

View File

@@ -1,348 +0,0 @@
/* eslint guard-for-in: 1 */
/* eslint camelcase: 1 */
(function() {
// Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing
var Context = function() {
this._calls = []; // names/args of recorded calls
this._initMethods();
this._fillStyle = null;
this._lineCap = null;
this._lineDashOffset = null;
this._lineJoin = null;
this._lineWidth = null;
this._strokeStyle = null;
// Define properties here so that we can record each time they are set
Object.defineProperties(this, {
fillStyle: {
get: function() {
return this._fillStyle;
},
set: function(style) {
this._fillStyle = style;
this.record('setFillStyle', [style]);
}
},
lineCap: {
get: function() {
return this._lineCap;
},
set: function(cap) {
this._lineCap = cap;
this.record('setLineCap', [cap]);
}
},
lineDashOffset: {
get: function() {
return this._lineDashOffset;
},
set: function(offset) {
this._lineDashOffset = offset;
this.record('setLineDashOffset', [offset]);
}
},
lineJoin: {
get: function() {
return this._lineJoin;
},
set: function(join) {
this._lineJoin = join;
this.record('setLineJoin', [join]);
}
},
lineWidth: {
get: function() {
return this._lineWidth;
},
set: function(width) {
this._lineWidth = width;
this.record('setLineWidth', [width]);
}
},
strokeStyle: {
get: function() {
return this._strokeStyle;
},
set: function(style) {
this._strokeStyle = style;
this.record('setStrokeStyle', [style]);
}
},
});
};
Context.prototype._initMethods = function() {
// define methods to test here
// no way to introspect so we have to do some extra work :(
var methods = {
arc: function() {},
beginPath: function() {},
bezierCurveTo: function() {},
clearRect: function() {},
closePath: function() {},
fill: function() {},
fillRect: function() {},
fillText: function() {},
lineTo: function() {},
measureText: function(text) {
// return the number of characters * fixed size
return text ? {width: text.length * 10} : {width: 0};
},
moveTo: function() {},
quadraticCurveTo: function() {},
restore: function() {},
rotate: function() {},
save: function() {},
setLineDash: function() {},
stroke: function() {},
strokeRect: function() {},
setTransform: function() {},
translate: function() {},
};
// attach methods to the class itself
var me = this;
var methodName;
var addMethod = function(name, method) {
me[methodName] = function() {
me.record(name, arguments);
return method.apply(me, arguments);
};
};
for (methodName in methods) {
var method = methods[methodName];
addMethod(methodName, method);
}
};
Context.prototype.record = function(methodName, args) {
this._calls.push({
name: methodName,
args: Array.prototype.slice.call(args)
});
};
Context.prototype.getCalls = function() {
return this._calls;
};
Context.prototype.resetCalls = function() {
this._calls = [];
};
window.createMockContext = function() {
return new Context();
};
// Custom matcher
function toBeCloseToPixel() {
return {
compare: function(actual, expected) {
var result = false;
if (!isNaN(actual) && !isNaN(expected)) {
var diff = Math.abs(actual - expected);
var A = Math.abs(actual);
var B = Math.abs(expected);
var percentDiff = 0.005; // 0.5% diff
result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
}
return {pass: result};
}
};
}
function toEqualOneOf() {
return {
compare: function(actual, expecteds) {
var result = false;
for (var i = 0, l = expecteds.length; i < l; i++) {
if (actual === expecteds[i]) {
result = true;
break;
}
}
return {
pass: result
};
}
};
}
function toBeValidChart() {
return {
compare: function(actual) {
var message = null;
if (!(actual instanceof Chart)) {
message = 'Expected ' + actual + ' to be an instance of Chart';
} else if (!(actual.canvas instanceof HTMLCanvasElement)) {
message = 'Expected canvas to be an instance of HTMLCanvasElement';
} else if (!(actual.ctx instanceof CanvasRenderingContext2D)) {
message = 'Expected context to be an instance of CanvasRenderingContext2D';
} else if (typeof actual.height !== 'number' || !isFinite(actual.height)) {
message = 'Expected height to be a strict finite number';
} else if (typeof actual.width !== 'number' || !isFinite(actual.width)) {
message = 'Expected width to be a strict finite number';
}
return {
message: message? message : 'Expected ' + actual + ' to be valid chart',
pass: !message
};
}
};
}
function toBeChartOfSize() {
return {
compare: function(actual, expected) {
var res = toBeValidChart().compare(actual);
if (!res.pass) {
return res;
}
var message = null;
var canvas = actual.ctx.canvas;
var style = getComputedStyle(canvas);
var pixelRatio = window.devicePixelRatio;
var dh = parseInt(style.height, 10);
var dw = parseInt(style.width, 10);
var rh = canvas.height;
var rw = canvas.width;
var orh = rh / pixelRatio;
var orw = rw / pixelRatio;
// sanity checks
if (actual.height !== orh) {
message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh;
} else if (actual.width !== orw) {
message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw;
}
// validity checks
if (dh !== expected.dh) {
message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
} else if (dw !== expected.dw) {
message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
} else if (rh !== expected.rh) {
message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
} else if (rw !== expected.rw) {
message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
}
return {
message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
pass: !message
};
}
};
}
beforeEach(function() {
jasmine.addMatchers({
toBeCloseToPixel: toBeCloseToPixel,
toEqualOneOf: toEqualOneOf,
toBeValidChart: toBeValidChart,
toBeChartOfSize: toBeChartOfSize
});
});
// Canvas injection helpers
var charts = {};
/**
* Injects a new canvas (and div wrapper) and creates teh associated Chart instance
* using the given config. Additional options allow tweaking elements generation.
* @param {object} config - Chart config.
* @param {object} options - Chart acquisition options.
* @param {object} options.canvas - Canvas attributes.
* @param {object} options.wrapper - Canvas wrapper attributes.
* @param {boolean} options.persistent - If true, the chart will not be released after the spec.
*/
function acquireChart(config, options) {
var wrapper = document.createElement('div');
var canvas = document.createElement('canvas');
var chart, key;
config = config || {};
options = options || {};
options.canvas = options.canvas || {height: 512, width: 512};
options.wrapper = options.wrapper || {class: 'chartjs-wrapper'};
for (key in options.canvas) {
if (options.canvas.hasOwnProperty(key)) {
canvas.setAttribute(key, options.canvas[key]);
}
}
for (key in options.wrapper) {
if (options.wrapper.hasOwnProperty(key)) {
wrapper.setAttribute(key, options.wrapper[key]);
}
}
// by default, remove chart animation and auto resize
config.options = config.options || {};
config.options.animation = config.options.animation === undefined? false : config.options.animation;
config.options.responsive = config.options.responsive === undefined? false : config.options.responsive;
config.options.defaultFontFamily = config.options.defaultFontFamily || 'Arial';
wrapper.appendChild(canvas);
window.document.body.appendChild(wrapper);
chart = new Chart(canvas.getContext('2d'), config);
chart._test_persistent = options.persistent;
chart._test_wrapper = wrapper;
charts[chart.id] = chart;
return chart;
}
function releaseChart(chart) {
chart.destroy();
chart._test_wrapper.remove();
delete charts[chart.id];
}
afterEach(function() {
// Auto releasing acquired charts
for (var id in charts) {
var chart = charts[id];
if (!chart._test_persistent) {
releaseChart(chart);
}
}
});
function injectCSS(css) {
// http://stackoverflow.com/q/3922139
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.setAttribute('type', 'text/css');
if (style.styleSheet) { // IE
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
window.acquireChart = acquireChart;
window.releaseChart = releaseChart;
// some style initialization to limit differences between browsers across different plateforms.
injectCSS(
'.chartjs-wrapper, .chartjs-wrapper canvas {' +
'border: 0;' +
'margin: 0;' +
'padding: 0;' +
'}' +
'.chartjs-wrapper {' +
'position: absolute' +
'}');
}());