diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..2b6413e9
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,33 @@
+version: 2
+
+jobs:
+ build:
+ docker:
+ # see https://circleci.com/docs/2.0/circleci-images/
+ # use '-browsers' version to have access to xvfb wrappers
+ - image: circleci/node:6.10.3-browsers
+
+ steps:
+ - checkout
+
+ # Download and cache dependencies
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "package.json" }}
+ # fallback to using the latest cache if no exact match is found
+ - v1-dependencies-
+
+ - run: npm install
+
+ - save_cache:
+ paths:
+ - node_modules
+ key: v1-dependencies-{{ checksum "package.json" }}
+
+ # install pdftops
+ - run: sudo apt-get install poppler-utils
+
+ - run: npm test
+
+ - store_artifacts:
+ path: build
diff --git a/bin/args.js b/bin/args.js
index 0e187a05..7b176bcf 100644
--- a/bin/args.js
+++ b/bin/args.js
@@ -1,7 +1,7 @@
const plotlyGraphCst = require('../src/component/plotly-graph/constants')
const minimist = require('minimist')
-const PLOTLYJS_STRING = ['plotly', 'mapbox-access-token', 'topojson', 'mathjax', 'batik']
+const PLOTLYJS_STRING = ['plotly', 'mapbox-access-token', 'topojson', 'mathjax']
const PLOTLYJS_ALIAS = {
'plotly': ['plotlyjs', 'plotly-js', 'plotly_js', 'plotlyJS', 'plotlyJs'],
@@ -13,8 +13,7 @@ const PLOTLYJS_DEFAULT = {
'plotly': '',
'mapbox-access-token': process.env.MAPBOX_ACCESS_TOKEN || '',
'topojson': '',
- 'mathjax': '',
- 'batik': process.env.BATIK_RASTERIZER_PATH || ''
+ 'mathjax': ''
}
const DESCRIPTION = {
@@ -32,9 +31,7 @@ const DESCRIPTION = {
topojson: `Sets path to topojson files. By default topojson files on the plot.ly CDN are used.`,
- mathjax: `Sets path to MathJax files. Required to export LaTeX characters.`,
-
- batik: 'Sets path to batik-rasterizer jar file. Required to export PDF and EPS formats.'
+ mathjax: `Sets path to MathJax files. Required to export LaTeX characters.`
}
const EXPORTER_MINIMIST_CONFIG = {
@@ -151,9 +148,6 @@ exports.getExporterHelpMsg = function () {
--mathjax ${formatAliases('mathjax')}
${DESCRIPTION.mathjax}
- --batik
- ${DESCRIPTION.batik}
-
--format ${formatAliases('format')}
Sets the output format (${Object.keys(plotlyGraphCst.contentFormat).join(', ')}). Applies to all output images.
@@ -219,9 +213,6 @@ exports.getServerHelpMsg = function () {
--mathjax ${formatAliases('mathjax')}
${DESCRIPTION.mathjax}
- --batik
- ${DESCRIPTION.batik}
-
--debug
${DESCRIPTION.debug}
`
diff --git a/bin/plotly-export-server_electron.js b/bin/plotly-export-server_electron.js
index 3e2a7a1e..44e58b47 100644
--- a/bin/plotly-export-server_electron.js
+++ b/bin/plotly-export-server_electron.js
@@ -1,5 +1,4 @@
const plotlyExporter = require('../')
-const Batik = require('../src/util/batik')
const { getServerArgs, getServerHelpMsg } = require('./args')
const pkg = require('../package.json')
@@ -20,32 +19,12 @@ if (argv.help) {
// - try https://github.com/indexzero/node-portfinder
let app
-let batik
-
-if (argv.batik) {
- if (!Batik.isJavaInstalled()) {
- console.warn('Missing binaries for PDF exports')
- process.exit(1)
- }
- if (!Batik.isPdftopsInstalled()) {
- console.warn('Missing binaries for EPS exports')
- process.exit(1)
- }
-
- batik = new Batik(argv.batik)
-
- if (!batik.doesBatikJarExist()) {
- console.warn('Path to batik-rasterizer jar file does not exist')
- process.exit(1)
- }
-}
const plotlyJsOpts = {
plotlyJS: argv.plotlyJS,
mapboxAccessToken: argv['mapbox-access-token'],
mathjax: argv.mathjax,
- topojson: argv.topojson,
- batik: batik
+ topojson: argv.topojson
}
const opts = {
diff --git a/bin/plotly-graph-exporter_electron.js b/bin/plotly-graph-exporter_electron.js
index ce3f593b..42fb2e18 100644
--- a/bin/plotly-graph-exporter_electron.js
+++ b/bin/plotly-graph-exporter_electron.js
@@ -58,7 +58,6 @@ getStdin().then((txt) => {
mapboxAccessToken: argv['mapbox-access-token'],
mathjax: argv.mathjax,
topojson: argv.topojson,
- batik: argv.batik || path.join(__dirname, '..', 'build', 'batik-1.7', 'batik-rasterizer.jar'),
format: argv.format,
scale: argv.scale,
width: argv.width,
diff --git a/package.json b/package.json
index 99634dbf..4bb59446 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"plotly-export-server": "./bin/plotly-export-server.js"
},
"scripts": {
+ "pretest": "node test/pretest.js",
"test:lint": "standard | snazzy",
"test:unit": "tap test/unit/*_test.js",
"test:integration": "tap test/integration/*_test.js",
@@ -35,6 +36,7 @@
"request": "^2.81.0",
"run-parallel": "^1.1.6",
"run-parallel-limit": "^1.0.3",
+ "run-series": "^1.1.4",
"semver": "^5.4.1",
"string-to-stream": "^1.1.0",
"uuid": "^3.1.0"
diff --git a/src/component/plotly-dashboard/constants.js b/src/component/plotly-dashboard/constants.js
new file mode 100644
index 00000000..7b6ebf55
--- /dev/null
+++ b/src/component/plotly-dashboard/constants.js
@@ -0,0 +1,7 @@
+module.exports = {
+ iframeLoadDelay: 5000,
+
+ statusMsg: {
+ 525: 'print to PDF error'
+ }
+}
diff --git a/src/component/plotly-dashboard/render.js b/src/component/plotly-dashboard/render.js
index a5fb5a17..6e01a910 100644
--- a/src/component/plotly-dashboard/render.js
+++ b/src/component/plotly-dashboard/render.js
@@ -1,4 +1,5 @@
-const IFRAME_LOAD_TIMEOUT = 5000
+const remote = require('../../util/remote')
+const cst = require('./constants')
/**
* @param {object} info : info object
@@ -12,56 +13,48 @@ const IFRAME_LOAD_TIMEOUT = 5000
* - imgData
*/
function render (info, opts, sendToMain) {
- // Cannot require 'remote' in the module scope
- // as this file gets required in main process first
- // during the coerce-component step
- //
- // TODO
- // - maybe require this in from create-index,
- // so that we don't have to worry about requiring it
- // inside the function body AND to make mockable for testing
- const {BrowserWindow} = require('electron').remote
-
- let win = new BrowserWindow({
+ let win = remote.createBrowserWindow({
width: info.width,
height: info.height
})
+ const contents = win.webContents
const result = {}
- let contents = win.webContents
+ let errorCode = null
const done = () => {
win.close()
+
+ if (errorCode) {
+ result.msg = cst.statusMsg[errorCode]
+ }
+ sendToMain(errorCode, result)
}
win.on('closed', () => {
win = null
})
- // ... or plain index.html + `win.executeJavascript`
- win.loadURL(info.url)
-
// TODO
// - find better solution than IFRAME_LOAD_TIMEOUT
// - but really, we shouldn't be using iframes in embed view?
// - use `content.capturePage` to render PNGs and JPEGs
- // - or use batik?
contents.once('did-finish-load', () => {
setTimeout(() => {
contents.printToPDF({}, (err, imgData) => {
if (err) {
- result.msg = 'print to PDF error'
- sendToMain(525, result)
+ errorCode = 525
return done()
}
result.imgData = imgData
- sendToMain(null, result)
- done()
+ return done()
})
- }, IFRAME_LOAD_TIMEOUT)
+ }, cst.iframeLoadDelay)
})
+
+ win.loadURL(info.url)
}
module.exports = render
diff --git a/src/component/plotly-graph/constants.js b/src/component/plotly-graph/constants.js
index bd837092..eb19c2cf 100644
--- a/src/component/plotly-graph/constants.js
+++ b/src/component/plotly-graph/constants.js
@@ -28,5 +28,11 @@ module.exports = {
svg: /^data:image\/svg\+xml,/
},
- mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG'
+ mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG',
+
+ // config option passed in render step
+ plotGlPixelRatio: 3,
+
+ // time [in ms] after which printToPDF errors when image isn't loaded
+ pdfPageLoadImgTimeout: 2000
}
diff --git a/src/component/plotly-graph/convert.js b/src/component/plotly-graph/convert.js
index c2fb98ba..b9e885d4 100644
--- a/src/component/plotly-graph/convert.js
+++ b/src/component/plotly-graph/convert.js
@@ -1,4 +1,4 @@
-const Batik = require('../../util/batik')
+const Pdftops = require('../../util/pdftops')
const cst = require('./constants')
/** plotly-graph convert
@@ -7,7 +7,7 @@ const cst = require('./constants')
* - format {string} (from parse)
* - imgData {string} (from render)
* @param {object} opts : component options
- * - batik {string or instance of Batik}
+ * - pdftops {string or instance of Pdftops)
* @param {function} reply
* - errorCode {number or null}
* - result {object}
@@ -19,59 +19,63 @@ function convert (info, opts, reply) {
const imgData = info.imgData
const format = info.format
- const result = {
- head: {'Content-Type': cst.contentFormat[format]}
- }
-
+ const result = {}
let errorCode = null
+ let body
+ let bodyLength
const done = () => {
if (errorCode) {
result.msg = cst.statusMsg[errorCode]
+ } else {
+ result.body = body
+ result.bodyLength = bodyLength
+ result.head = {
+ 'Content-Type': cst.contentFormat[format],
+ 'Content-Length': bodyLength
+ }
}
reply(errorCode, result)
}
+ const pdf2eps = (pdf, cb) => {
+ const pdftops = opts.pdftops instanceof Pdftops
+ ? opts.pdftops
+ : new Pdftops(opts.pdftops)
+
+ pdftops.pdf2eps(pdf, {id: info.id}, (err, eps) => {
+ if (err) {
+ errorCode = 530
+ result.error = err
+ return done()
+ }
+ cb(eps)
+ })
+ }
+
// TODO
- // - should pdf and eps format be part of a streambed-only component?
- // - should we use batik for that or something?
// - is the 'encoded' option still relevant?
switch (format) {
case 'png':
case 'jpeg':
case 'webp':
- const body = result.body = Buffer.from(imgData, 'base64')
- result.bodyLength = result.head['Content-Length'] = body.length
+ case 'pdf':
+ body = Buffer.from(imgData, 'base64')
+ bodyLength = body.length
return done()
case 'svg':
// see http://stackoverflow.com/a/12205668/800548
- result.body = imgData
- result.bodyLength = encodeURI(imgData).split(/%..|./).length - 1
+ body = imgData
+ bodyLength = encodeURI(imgData).split(/%..|./).length - 1
return done()
- case 'pdf':
case 'eps':
- if (!opts.batik) {
- errorCode = 530
- result.error = new Error('path to batik-rasterizer jar not given')
+ pdf2eps(imgData, (eps) => {
+ body = eps
+ bodyLength = body.length
return done()
- }
-
- const batik = opts.batik instanceof Batik
- ? opts.batik
- : new Batik(opts.batik)
-
- batik.convertSVG(info.imgData, {format: format}, (err, buf) => {
- if (err) {
- errorCode = 530
- result.error = err
- return done()
- }
-
- result.bodyLength = result.head['Content-Length'] = buf.length
- result.body = buf
- done()
})
+ break
}
}
diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js
index 08d84995..22a797a4 100644
--- a/src/component/plotly-graph/render.js
+++ b/src/component/plotly-graph/render.js
@@ -1,7 +1,8 @@
/* global Plotly:false */
-const cst = require('./constants')
const semver = require('semver')
+const remote = require('../../util/remote')
+const cst = require('./constants')
/**
* @param {object} info : info object
@@ -21,10 +22,10 @@ function render (info, opts, sendToMain) {
const figure = info.figure
const format = info.format
- const config = Object.assign({},
- {mapboxAccessToken: opts.mapboxAccessToken || ''},
- figure.config
- )
+ const config = Object.assign({
+ mapboxAccessToken: opts.mapboxAccessToken || '',
+ plotGlPixelRatio: cst.plotGlPixelRatio
+ }, figure.config)
const result = {}
let errorCode = null
@@ -36,30 +37,41 @@ function render (info, opts, sendToMain) {
sendToMain(errorCode, result)
}
- // TODO
- // - figure out if we still need this:
- // https://github.com/plotly/streambed/blob/7311d4386d80d45999797e87992f43fb6ecf48a1/image_server/server_app/main.js#L224-L229
- // - increase pixel ratio images (scale up here, scale down in convert) ??
- // - does webp (via batik) support transparency now?
+ const PRINT_TO_PDF = (format === 'pdf' || format === 'eps')
+
+ // stash `paper_bgcolor` here in order to set the pdf window bg color
+ let bgColor
+ const pdfBackground = (gd, _bgColor) => {
+ if (!bgColor) bgColor = _bgColor
+ gd._fullLayout.paper_bgcolor = 'rgba(0,0,0,0)'
+ }
const imgOpts = {
- format: (format === 'pdf' || format === 'eps') ? 'svg' : format,
- width: info.scale * info.width,
- height: info.scale * info.height,
+ format: PRINT_TO_PDF ? 'svg' : format,
+ width: info.width,
+ height: info.height,
+ // works as of https://github.com/plotly/plotly.js/compare/to-image-scale
+ scale: info.scale,
// return image data w/o the leading 'data:image' spec
- imageDataOnly: true,
+ imageDataOnly: !PRINT_TO_PDF,
// blend jpeg background color as jpeg does not support transparency
- setBackground: format === 'jpeg' ? 'opaque' : ''
+ setBackground: format === 'jpeg' ? 'opaque'
+ : PRINT_TO_PDF ? pdfBackground
+ : ''
}
let promise
if (semver.gte(Plotly.version, '1.30.0')) {
- promise = Plotly.toImage({
- data: figure.data,
- layout: figure.layout,
- config: config
- }, imgOpts)
+ promise = Plotly
+ .toImage({data: figure.data, layout: figure.layout, config: config}, imgOpts)
+ .then((imgData) => {
+ if (PRINT_TO_PDF) {
+ return toPDF(imgData, imgOpts, bgColor)
+ } else {
+ return imgData
+ }
+ })
} else if (semver.gte(Plotly.version, '1.11.0')) {
const gd = document.createElement('div')
@@ -69,13 +81,16 @@ function render (info, opts, sendToMain) {
.then((imgData) => {
Plotly.purge(gd)
- switch (imgOpts.format) {
+ switch (format) {
case 'png':
case 'jpeg':
case 'webp':
return imgData.replace(cst.imgPrefix.base64, '')
case 'svg':
- return decodeURIComponent(imgData.replace(cst.imgPrefix.svg, ''))
+ return decodeSVG(imgData)
+ case 'pdf':
+ case 'eps':
+ return toPDF(imgData, imgOpts, bgColor)
}
})
} else {
@@ -95,4 +110,88 @@ function render (info, opts, sendToMain) {
})
}
+function decodeSVG (imgData) {
+ return window.decodeURIComponent(imgData.replace(cst.imgPrefix.svg, ''))
+}
+
+function toPDF (imgData, imgOpts, bgColor) {
+ const wPx = imgOpts.scale * imgOpts.width
+ const hPx = imgOpts.scale * imgOpts.height
+
+ const pxByMicrometer = 0.00377957517575025
+ const offset = 6
+
+ // See other available options:
+ // https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsprinttopdfoptions-callback
+ const printOpts = {
+ // no margins
+ marginsType: 1,
+ // make bg (set to `paper_bgcolor` value) appear in export
+ printBackground: true,
+ // printToPDF expects page size setting in micrometer (1e-6 m)
+ // - Px by micrometer factor is taken from
+ // https://www.translatorscafe.com/unit-converter/en/length/13-110/micrometer-pixel/
+ // - Even under the `marginsType: 1` setting (meaning no margins), printToPDF still
+ // outputs small margins. We need to take this into consideration so that the output PDF
+ // does not span multiple pages. The offset value was found empirically via trial-and-error.
+ pageSize: {
+ width: (wPx + offset) / pxByMicrometer,
+ height: (hPx + offset) / pxByMicrometer
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ let win = remote.createBrowserWindow({
+ width: wPx,
+ height: hPx
+ })
+
+ win.on('closed', () => {
+ win = null
+ })
+
+ const html = window.encodeURIComponent(`
+
+
+
+
+
+
+ `)
+
+ // we can't set image src into html as chromium has a 2MB URL limit
+ // https://craignicol.wordpress.com/2016/07/19/excellent-export-and-the-chrome-url-limit/
+
+ win.loadURL(`data:text/html,${html}`)
+
+ win.webContents.executeJavaScript(`new Promise((resolve, reject) => {
+ const img = document.body.firstChild
+ img.onload = resolve
+ img.onerror = reject
+ img.src = "${imgData}"
+ setTimeout(() => reject(new Error('too long to load image')), ${cst.pdfPageLoadImgTimeout})
+ })`)
+ .then(() => {
+ win.webContents.printToPDF(printOpts, (err, pdfData) => {
+ if (err) {
+ reject(err)
+ } else {
+ resolve(pdfData)
+ }
+ win.close()
+ })
+ })
+ .catch((err) => {
+ reject(err)
+ win.close()
+ })
+ })
+}
+
module.exports = render
diff --git a/src/util/batik.js b/src/util/batik.js
deleted file mode 100644
index 94b06466..00000000
--- a/src/util/batik.js
+++ /dev/null
@@ -1,177 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-const childProcess = require('child_process')
-const parallel = require('run-parallel')
-const uuid = require('uuid/v4')
-
-const PATH_TO_BUILD = path.join(__dirname, '..', '..', 'build')
-
-/** Node wrapper for Batik
- *
- * See:
- * - https://xmlgraphics.apache.org/batik/tools/rasterizer.html
- * - http://archive.apache.org/dist/xmlgraphics/batik/
- */
-class Batik {
- /** Create batik rasterizer instance
- *
- * @param {string} batikJar : path to batik rasterizer jar file
- */
- constructor (batikJar) {
- this.batikJar = path.resolve(batikJar)
-
- this.bg = '255.255.255.255'
- this.dpi = '300'
- this.javaBase = 'java -jar -XX:+UseParallelGC -server'
- this.batikBase = `${this.javaBase} ${this.batikJar}`
- this.pdftopsBase = 'pdftops'
- }
-
- /** Convert svg input
- *
- * @param {string} svg : svg string to convert
- * @param {object} opts :
- * - id {string}
- * - format {string}
- * - width {numeric}
- * - height {numeric}
- * @param {function} cb
- * - err {null || error}
- * - result {buffer}
- */
- convertSVG (svg, opts, cb) {
- const id = opts.id || uuid()
- const format = opts.format || 'pdf'
- const width = opts.width
- const height = opts.height
- const isEPS = format === 'eps'
-
- // TODO old batik wrapper had:
- //
- // svg = svg.replace(/(font-family: )('Courier New')/g, '$1\'Liberation mono\'')
- // .replace(/(font-family: )('Times New Roman')/g, '$1\'Liberation Serif\'')
- // .replace(/(font-family: )(Arial)/g, '$1\'Liberation Sans\'')
- // .replace(/(font-family: )(Balto)/g, '$1\'Balto Book\'')
- // .replace(/(font-family: )(Tahoma)/g, '$1\'Liberation Sans\'');
- //
- // find out why, and check if it's still needed.
-
- const lookup = {
- out: tmpFile(id + '-out', ''),
- // TODO do we still need this `\ufeff` addition?
- svg: tmpFile(id + '-svg', '\ufeff' + svg)
- }
-
- if (isEPS) {
- lookup.eps = tmpFile(id + '-eps', '')
- }
-
- const fileNames = Object.keys(lookup)
- const createTasks = fileNames.map(k => lookup[k].create)
- const destroyTasks = fileNames.map(k => lookup[k].destroy)
-
- let args
-
- switch (format) {
- case 'pdf':
- case 'eps':
- args = `-m application/pdf`
- break
- default:
- args = `-bg ${this.bg} -dpi ${this.dpi} `
- if (width) args += `-w ${width} `
- if (height) args += `-h ${height} `
- break
- }
-
- const done = (err, out) => parallel(destroyTasks, (err2) => {
- if (err) {
- return cb(err)
- }
- if (err2) {
- return cb(new Error('problem while removing temporary files'))
- }
- cb(null, out)
- })
-
- const toBuffer = (outPath) => {
- fs.readFile(outPath, (err, buf) => {
- if (err) {
- return done(new Error('problem while reading output file'))
- }
- done(null, buf)
- })
- }
-
- // go!
- parallel(createTasks, (err) => {
- if (err) {
- return done(new Error('problem while initializing temporary files'))
- }
-
- const batikCmd = `${this.batikBase} ${args} -d ${lookup.out.path} ${lookup.svg.path}`
-
- childProcess.exec(batikCmd, (err) => {
- if (err) {
- return done(new Error('problem while executing batik command'))
- }
-
- if (isEPS) {
- const pdftopsCmd = `${this.pdftopsBase} -eps ${lookup.out.path} ${lookup.eps.path}`
-
- childProcess.exec(pdftopsCmd, (err) => {
- if (err) {
- return done(new Error('problem while executing pdftops command'))
- }
- toBuffer(lookup.eps.path)
- })
- } else {
- toBuffer(lookup.out.path)
- }
- })
- })
- }
-
- /** Does batik-rasterizer jar file exist?
- * @return {boolean}
- */
- doesBatikJarExist () {
- return fs.existsSync(this.batikJar)
- }
-
- /** Is java installed?
- * @return {boolean}
- */
- static isJavaInstalled () {
- try {
- childProcess.execSync('java -version', {stdio: 'ignore'})
- } catch (e) {
- return false
- }
- return true
- }
-
- /** Is pdftops installed?
- * @return {boolean}
- */
- static isPdftopsInstalled () {
- try {
- childProcess.execSync('pdftops -v', {stdio: 'ignore'})
- } catch (e) {
- return false
- }
- return true
- }
-}
-
-function tmpFile (fileName, content) {
- var pathToFile = path.join(PATH_TO_BUILD, fileName)
-
- return {
- path: pathToFile,
- create: (cb) => fs.writeFile(pathToFile, content, 'utf8', cb),
- destroy: (cb) => fs.unlink(pathToFile, cb)
- }
-}
-
-module.exports = Batik
diff --git a/src/util/pdftops.js b/src/util/pdftops.js
new file mode 100644
index 00000000..8a2304b8
--- /dev/null
+++ b/src/util/pdftops.js
@@ -0,0 +1,67 @@
+const fs = require('fs')
+const path = require('path')
+const childProcess = require('child_process')
+const parallel = require('run-parallel')
+const series = require('run-series')
+const uuid = require('uuid/v4')
+
+const PATH_TO_BUILD = path.join(__dirname, '..', '..', 'build')
+
+/** Node wrapper for pdftops
+ *
+ * $ apt-get poppler-utils
+ *
+ * See:
+ * - https://linux.die.net/man/1/pdftops
+ * - https://en.wikipedia.org/wiki/Poppler_(software)#poppler-utils
+ */
+class Pdftops {
+ constructor (pathToPdftops) {
+ this.cmdBase = pathToPdftops || 'pdftops'
+ }
+
+ /** Convert PDF to EPS
+ *
+ * @param {buffer} pdf : pdf data buffer
+ * @param {object} opts
+ * - id {string}
+ * @param {function} cb
+ * - err {null || error}
+ * - result {buffer}
+ */
+ pdf2eps (pdf, opts, cb) {
+ const id = opts.id || uuid()
+ const inPath = path.join(PATH_TO_BUILD, id + '-pdf')
+ const outPath = path.join(PATH_TO_BUILD, id + '-eps')
+ const cmd = `${this.cmdBase} -eps ${inPath} ${outPath}`
+
+ const destroyTmpFiles = (cb) => parallel([
+ (cb) => fs.unlink(inPath, cb),
+ (cb) => fs.unlink(outPath, cb)
+ ], cb)
+
+ series([
+ (cb) => fs.writeFile(inPath, pdf, 'utf-8', cb),
+ (cb) => childProcess.exec(cmd, cb),
+ (cb) => fs.readFile(outPath, cb)
+ ], (err, result) => {
+ destroyTmpFiles(() => {
+ cb(err, result[2])
+ })
+ })
+ }
+
+ /** Is pdftops installed?
+ * @return {boolean}
+ */
+ static isPdftopsInstalled () {
+ try {
+ childProcess.execSync(`${this.cmdBase} -v`, {stdio: 'ignore'})
+ } catch (e) {
+ return false
+ }
+ return true
+ }
+}
+
+module.exports = Pdftops
diff --git a/src/util/remote.js b/src/util/remote.js
new file mode 100644
index 00000000..20fbb15e
--- /dev/null
+++ b/src/util/remote.js
@@ -0,0 +1,18 @@
+/* Small wrapper around the renderer process 'remote' module, to
+ * easily mock it using sinon in test/common.js
+ *
+ * More info on the remote module:
+ * - https://electron.atom.io/docs/api/remote/
+ */
+
+function load () {
+ return require('electron').remote
+}
+
+module.exports = {
+ createBrowserWindow: (opts) => {
+ const _module = load()
+ return new _module.BrowserWindow(opts)
+ },
+ getCurrentWindow: () => load().getCurrentWindow()
+}
diff --git a/test/common.js b/test/common.js
index 8557b59c..68d20060 100644
--- a/test/common.js
+++ b/test/common.js
@@ -1,11 +1,15 @@
+const fs = require('fs')
const path = require('path')
+const EventEmitter = require('events')
+const sinon = require('sinon')
const paths = {}
const urls = {}
+const mocks = {}
paths.root = path.join(__dirname, '..')
paths.build = path.join(paths.root, 'build')
-paths.batik = process.env.BATIK_RASTERIZER_PATH || path.join(paths.build, 'batik-1.7', 'batik-rasterizer.jar')
+paths.bin = path.join(paths.root, 'bin')
paths.readme = path.join(paths.root, 'README.md')
paths.pkg = path.join(paths.root, 'package.json')
paths.glob = path.join(paths.root, 'src', 'util', '*')
@@ -13,7 +17,41 @@ paths.glob = path.join(paths.root, 'src', 'util', '*')
urls.dummy = 'http://dummy.url'
urls.plotlyGraphMock = 'https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/20.json'
+try {
+ mocks.svg = fs.readFileSync(path.join(paths.build, 'test-mock.svg'), 'utf-8')
+ mocks.pdf = fs.readFileSync(path.join(paths.build, 'test-mock.pdf'))
+} catch (e) {}
+
+function createMockWindow (opts = {}) {
+ const win = new EventEmitter()
+ const webContents = new EventEmitter()
+
+ webContents.executeJavaScript = sinon.stub()
+ webContents.printToPDF = sinon.stub()
+
+ Object.assign(win, opts, {
+ webContents: webContents,
+ loadURL: sinon.stub().callsFake(() => {
+ webContents.emit('did-finish-load')
+ }),
+ close: sinon.stub().callsFake(() => {
+ win.emit('closed')
+ })
+ })
+
+ return win
+}
+
+function stubProp (obj, key, newVal) {
+ const oldVal = obj[key]
+ obj[key] = newVal
+ return () => { obj[key] = oldVal }
+}
+
module.exports = {
paths: paths,
- urls: urls
+ urls: urls,
+ mocks: mocks,
+ createMockWindow: createMockWindow,
+ stubProp: stubProp
}
diff --git a/test/integration/plotly-graph-exporter_test.js b/test/integration/plotly-graph-exporter_test.js
index 4a582624..7cc5e47e 100644
--- a/test/integration/plotly-graph-exporter_test.js
+++ b/test/integration/plotly-graph-exporter_test.js
@@ -52,24 +52,3 @@ tap.test('should print help message', t => {
t.end()
})
-
-tap.test('should print export info on success', t => {
- const input = '{"data":[{"y":[1,2,1]}],"layout":{"title":"tester"}}'
- const subprocess = _spawn(t, input)
-
- const matches = [
- /^exported fig/,
- /done with code 0/
- ]
-
- let i = 0
-
- subprocess.stdout.on('data', d => {
- t.match(d.toString(), matches[i], `msg ${i}`)
- i++
- })
-
- subprocess.stderr.on('data', d => {
- t.fail(d, 'unwanted msg to stderr')
- })
-})
diff --git a/test/pretest.js b/test/pretest.js
new file mode 100644
index 00000000..733fd905
--- /dev/null
+++ b/test/pretest.js
@@ -0,0 +1,17 @@
+const { execSync } = require('child_process')
+const { paths } = require('./common')
+
+const mock = JSON.stringify({
+ data: [{
+ y: [1, 2, 1]
+ }],
+ layout: {
+ title: 'test mock'
+ }
+})
+
+execSync(`${paths.bin}/plotly-graph-exporter.js '${mock}' -f svg -d build -o test-mock`)
+console.log('build/test-mock.svg created')
+
+execSync(`${paths.bin}/plotly-graph-exporter.js '${mock}' -f pdf -d build -o test-mock`)
+console.log('build/test-mock.pdf created')
diff --git a/test/unit/batik_test.js b/test/unit/batik_test.js
deleted file mode 100644
index 65284c89..00000000
--- a/test/unit/batik_test.js
+++ /dev/null
@@ -1,214 +0,0 @@
-const tap = require('tap')
-const sinon = require('sinon')
-const path = require('path')
-const fs = require('fs')
-const childProcess = require('child_process')
-const imageSize = require('image-size')
-const Batik = require('../../src/util/batik')
-
-const { paths } = require('../common')
-const SVG_MOCK = fs.readFileSync(path.join(paths.build, '20.svg'))
-
-tap.test('Batik constructor:', t => {
- t.test('', t => {
- const batik = new Batik(paths.batik)
- t.type(batik.convertSVG, 'function')
- t.end()
- })
-
- t.end()
-})
-
-tap.test('batik.convertSVG', t => {
- t.test('should convert svg to pdf', t => {
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'batik-test.pdf')
-
- batik.convertSVG(SVG_MOCK, {format: 'pdf'}, (err, result) => {
- if (err) t.fail(err)
- t.type(result, Buffer)
-
- fs.writeFile(outPath, result, (err) => {
- if (err) t.fail(err)
-
- const size = fs.statSync(outPath).size
- t.ok(size > 5e4, 'min pdf file size')
- t.ok(size < 7e4, 'max pdf file size')
- t.end()
- })
- })
- })
-
- t.test('should convert svg to eps', t => {
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'batik-test.eps')
-
- batik.convertSVG(SVG_MOCK, {format: 'eps'}, (err, result) => {
- if (err) t.fail(err)
- t.type(result, Buffer)
-
- fs.writeFile(outPath, result, (err) => {
- if (err) t.fail(err)
-
- const size = fs.statSync(outPath).size
- t.ok(size > 1e6, 'min eps file size')
- t.ok(size < 2e6, 'max eps file size')
- t.end()
- })
- })
- })
-
- t.test('should convert svg to png', t => {
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'batik-test.png')
-
- batik.convertSVG(SVG_MOCK, {format: 'png'}, (err, result) => {
- if (err) t.fail(err)
- t.type(result, Buffer)
-
- fs.writeFile(outPath, result, (err) => {
- if (err) t.fail(err)
-
- const {width, height} = imageSize(outPath)
- t.equal(width, 750, 'png width')
- t.equal(height, 600, 'png height')
- t.end()
- })
- })
- })
-
- t.test('should rescale svg into png', t => {
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'batik-test.png')
-
- batik.convertSVG(SVG_MOCK, {format: 'png', width: 200, height: 200}, (err, result) => {
- if (err) t.fail(err)
- t.type(result, Buffer)
-
- fs.writeFile(outPath, result, (err) => {
- if (err) t.fail(err)
-
- const {width, height} = imageSize(outPath)
- t.equal(width, 200, 'png width')
- t.equal(height, 200, 'png height')
- t.end()
- })
- })
- })
-
- t.test('should error out when batik command fails', t => {
- const batik = new Batik(paths.batik)
- batik.batikBase = 'not gonna work'
-
- batik.convertSVG(SVG_MOCK, {}, (err) => {
- t.throws(() => { throw err }, /problem while executing batik command/)
- t.end()
- })
- })
-
- t.test('should error out when pdftops command fails', t => {
- const batik = new Batik(paths.batik)
- batik.pdftopsBase = 'not gonna work'
-
- batik.convertSVG(SVG_MOCK, {format: 'eps'}, (err) => {
- t.throws(() => { throw err }, /problem while executing pdftops command/)
- t.end()
- })
- })
-
- t.test('should error out when something goes wrong when initializing files', t => {
- sinon.stub(fs, 'writeFile').yields(true)
-
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'tmp-out')
- const svgPath = path.join(paths.build, 'tmp-svg')
-
- batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => {
- t.throws(() => { throw err }, /problem while initializing temporary files/)
- t.notOk(fs.existsSync(outPath), 'clears tmp out file')
- t.notOk(fs.existsSync(svgPath), 'clears tmp svg file')
-
- fs.writeFile.restore()
- t.end()
- })
- })
-
- t.test('should error out when something goes wrong when reading output files', t => {
- sinon.stub(fs, 'readFile').yields(true)
-
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'tmp-out')
- const svgPath = path.join(paths.build, 'tmp-svg')
-
- batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => {
- t.throws(() => { throw err }, /problem while reading output file/)
- t.notOk(fs.existsSync(outPath), 'clears tmp out file')
- t.notOk(fs.existsSync(svgPath), 'clears tmp svg file')
-
- fs.readFile.restore()
- t.end()
- })
- })
-
- t.test('should error out when something goes wrong when removing temporary files', t => {
- sinon.stub(fs, 'unlink').yields(true)
-
- const batik = new Batik(paths.batik)
- const outPath = path.join(paths.build, 'tmp-out')
- const svgPath = path.join(paths.build, 'tmp-svg')
-
- batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => {
- t.throws(() => { throw err }, /problem while removing temporary files/)
- t.ok(fs.existsSync(outPath), 'cannot clear tmp out file')
- t.ok(fs.existsSync(svgPath), 'cannot clears tmp svg file')
-
- fs.unlink.restore()
- fs.unlinkSync(outPath)
- fs.unlinkSync(svgPath)
- t.notOk(fs.existsSync(outPath), 'clears tmp out file')
- t.notOk(fs.existsSync(svgPath), 'clears tmp svg file')
-
- t.end()
- })
- })
-
- t.end()
-})
-
-tap.test('doesBatikJarExist:', t => {
- t.test('should return true when it does exists', t => {
- const batik = new Batik(paths.batik)
- t.ok(batik.doesBatikJarExist())
- t.end()
- })
-
- t.test('should return false when it does not exists', t => {
- const batik = new Batik('not gonna work')
- t.notOk(batik.doesBatikJarExist())
- t.end()
- })
- t.end()
-})
-
-tap.test('isJavaInstalled / isPdftopsInstalled:', t => {
- t.afterEach((done) => {
- childProcess.execSync.restore()
- done()
- })
-
- t.test('should return true when binary execute correctly', t => {
- sinon.stub(childProcess, 'execSync').returns(true)
- t.ok(Batik.isJavaInstalled())
- t.ok(Batik.isPdftopsInstalled())
- t.end()
- })
-
- t.test('should return false when binary does not execute correctly', t => {
- sinon.stub(childProcess, 'execSync').throws()
- t.notOk(Batik.isJavaInstalled())
- t.notOk(Batik.isPdftopsInstalled())
- t.end()
- })
-
- t.end()
-})
diff --git a/test/unit/pdftops_test.js b/test/unit/pdftops_test.js
new file mode 100644
index 00000000..db11b27d
--- /dev/null
+++ b/test/unit/pdftops_test.js
@@ -0,0 +1,81 @@
+const tap = require('tap')
+const sinon = require('sinon')
+const path = require('path')
+const fs = require('fs')
+const childProcess = require('child_process')
+const Pdftops = require('../../src/util/pdftops')
+
+const { paths, mocks } = require('../common')
+
+tap.test('pdftops.pdf2eps', t => {
+ t.test('should convert pdf to eps', t => {
+ const pdftops = new Pdftops()
+ const outPath = path.join(paths.build, 'pdftops-test.eps')
+
+ pdftops.pdf2eps(mocks.pdf, {}, (err, result) => {
+ if (err) t.fail(err)
+ t.type(result, Buffer)
+
+ fs.writeFile(outPath, result, (err) => {
+ if (err) t.fail(err)
+
+ const size = fs.statSync(outPath).size
+ t.ok(size > 3e5, 'min pdf file size')
+ t.ok(size < 4e5, 'max pdf file size')
+ t.end()
+ })
+ })
+ })
+
+ t.test('should remove tmp files after conversion', t => {
+ const pdftops = new Pdftops()
+ const tmpOutPath = path.join(paths.build, 'tmp-eps')
+ const tmpSvgPath = path.join(paths.build, 'tmp-pdf')
+
+ pdftops.pdf2eps(mocks.pdf, {id: 'tmp'}, (err, result) => {
+ if (err) t.fail(err)
+
+ t.type(result, Buffer)
+ t.notOk(fs.existsSync(tmpOutPath), 'clears tmp eps file')
+ t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp pdf file')
+ t.end()
+ })
+ })
+
+ t.test('should error out when pdftops command fails', t => {
+ const pdftops = new Pdftops('not gonna work')
+
+ const tmpOutPath = path.join(paths.build, 'tmp-eps')
+ const tmpSvgPath = path.join(paths.build, 'tmp-pdf')
+
+ pdftops.pdf2eps(mocks.pdf, {id: 'tmp'}, (err) => {
+ t.throws(() => { throw err }, /Command failed/)
+ t.notOk(fs.existsSync(tmpOutPath), 'clears tmp eps file')
+ t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp pdf file')
+ t.end()
+ })
+ })
+
+ t.end()
+})
+
+tap.test('isPdftopsInstalled', t => {
+ t.afterEach((done) => {
+ childProcess.execSync.restore()
+ done()
+ })
+
+ t.test('should return true when binary execute correctly', t => {
+ sinon.stub(childProcess, 'execSync').returns(true)
+ t.ok(Pdftops.isPdftopsInstalled())
+ t.end()
+ })
+
+ t.test('should return false when binary does not execute correctly', t => {
+ sinon.stub(childProcess, 'execSync').throws()
+ t.notOk(Pdftops.isPdftopsInstalled())
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/unit/plotly-dashboard_test.js b/test/unit/plotly-dashboard_test.js
index 4868e8b1..41cfdab5 100644
--- a/test/unit/plotly-dashboard_test.js
+++ b/test/unit/plotly-dashboard_test.js
@@ -1,5 +1,10 @@
const tap = require('tap')
+const sinon = require('sinon')
+
const _module = require('../../src/component/plotly-dashboard')
+const cst = require('../../src/component/plotly-dashboard/constants')
+const remote = require('../../src/util/remote')
+const { createMockWindow, stubProp } = require('../common')
tap.test('parse:', t => {
const fn = _module.parse
@@ -21,3 +26,70 @@ tap.test('parse:', t => {
t.end()
})
+
+tap.test('render:', t => {
+ const fn = _module.render
+ const restoreIframeLoadDelay = stubProp(cst, 'iframeLoadDelay', 0)
+
+ t.afterEach((done) => {
+ remote.createBrowserWindow.restore()
+ done()
+ })
+
+ t.tearDown(() => {
+ restoreIframeLoadDelay()
+ })
+
+ t.test('should call printToPDF', t => {
+ const win = createMockWindow()
+ sinon.stub(remote, 'createBrowserWindow').returns(win)
+ win.webContents.printToPDF.yields(null, '-> image data <-')
+
+ fn({
+ width: 500,
+ height: 500,
+ url: 'dummy'
+ }, {}, (errorCode, result) => {
+ t.ok(win.webContents.printToPDF.calledOnce)
+ t.ok(win.close.calledOnce)
+ t.equal(errorCode, null, 'code')
+ t.equal(result.imgData, '-> image data <-', 'result')
+ t.end()
+ })
+ })
+
+ t.test('should handle printToPDF errors', t => {
+ const win = createMockWindow()
+ sinon.stub(remote, 'createBrowserWindow').returns(win)
+ win.webContents.printToPDF.yields(new Error('printToPDF error'))
+
+ fn({
+ width: 500,
+ height: 500,
+ url: 'dummy'
+ }, {}, (errorCode, result) => {
+ t.ok(win.webContents.printToPDF.calledOnce)
+ t.ok(win.close.calledOnce)
+ t.equal(errorCode, 525, 'code')
+ t.equal(result.msg, 'print to PDF error', 'error msg')
+ t.end()
+ })
+ })
+
+ t.test('should cleanup window ref if window is manually closed', t => {
+ const win = createMockWindow()
+ sinon.stub(remote, 'createBrowserWindow').returns(win)
+
+ fn({
+ width: 500,
+ height: 500,
+ url: 'dummy'
+ })
+
+ t.equal(win.listenerCount('closed'), 1)
+ win.on('closed', t.end)
+ win.emit('closed')
+ })
+
+ t.end()
+})
diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js
index 3212f291..75886762 100644
--- a/test/unit/plotly-graph_test.js
+++ b/test/unit/plotly-graph_test.js
@@ -1,8 +1,10 @@
const tap = require('tap')
const sinon = require('sinon')
+const Pdftops = require('../../src/util/pdftops')
const _module = require('../../src/component/plotly-graph')
-const { paths } = require('../common')
+const remote = require('../../src/util/remote')
+const { paths, mocks, createMockWindow } = require('../common')
tap.test('inject:', t => {
const fn = _module.inject
@@ -318,13 +320,13 @@ tap.test('convert:', t => {
const fn = _module.convert
t.test('should convert image data to buffer', t => {
- const formats = ['png', 'webp', 'jpeg']
+ const formats = ['png', 'webp', 'jpeg', 'pdf']
formats.forEach(f => {
t.test(`for format ${f}`, t => {
fn({imgData: 'asdfDFDASFsafadsf', format: f}, {}, (errorCode, result) => {
t.equal(errorCode, null)
- t.equal(result.head['Content-Type'], `image/${f}`)
+ t.match(result.head['Content-Type'], f)
t.type(result.body, Buffer)
t.type(result.bodyLength, 'number')
t.end()
@@ -345,18 +347,41 @@ tap.test('convert:', t => {
})
})
- t.test('should convert image data to pdf', t => {
- const info = {imgData: '', format: 'pdf'}
- const opts = {batik: paths.batik}
+ t.test('should convert pdf data to eps', t => {
+ const info = {imgData: mocks.pdf, format: 'eps'}
+
+ fn(info, {}, (errorCode, result) => {
+ t.equal(errorCode, null)
+ t.equal(result.head['Content-Type'], 'application/postscript')
+ t.type(result.body, Buffer)
+ t.end()
+ })
+ })
+
+ t.test('should convert pdf data to eps (while passing instance of Pdftops)', t => {
+ const info = {imgData: mocks.pdf, format: 'eps'}
+ const opts = {pdftops: new Pdftops()}
fn(info, opts, (errorCode, result) => {
t.equal(errorCode, null)
- t.equal(result.head['Content-Type'], 'application/pdf')
+ t.equal(result.head['Content-Type'], 'application/postscript')
t.type(result.body, Buffer)
t.end()
})
})
+ t.test('should pass pdftops errors', t => {
+ const info = {imgData: mocks.pdf, format: 'eps'}
+ const opts = {pdftops: 'not gonna work'}
+
+ fn(info, opts, (errorCode, result) => {
+ t.equal(errorCode, 530)
+ t.match(result.msg, 'image conversion error')
+ t.match(result.error.message, 'Command failed')
+ t.end()
+ })
+ })
+
t.end()
})
@@ -371,51 +396,93 @@ tap.test('render:', t => {
let Plotly
let document
+ let window
t.beforeEach((done) => {
global.Plotly = Plotly = {}
global.document = document = {}
+ global.window = window = {}
done()
})
t.afterEach((done) => {
delete global.Plotly
delete global.document
+ delete global.window
done()
})
const mock130 = () => {
Plotly.version = '1.30.0'
- Plotly.toImage = sinon.stub().returns(new Promise(resolve => {
- resolve('image data')
- }))
+ Plotly.toImage = sinon.stub().returns(
+ new Promise(resolve => resolve('image data'))
+ )
}
const mock110 = () => {
Plotly.version = '1.11.0'
- Plotly.toImage = sinon.stub().returns(new Promise(resolve => {
- resolve('image data')
- }))
- Plotly.newPlot = sinon.stub().returns(new Promise(resolve => {
- resolve({})
- }))
+ Plotly.toImage = sinon.stub().resolves(
+ new Promise(resolve => resolve('image data'))
+ )
+ Plotly.newPlot = sinon.stub().resolves(
+ new Promise(resolve => resolve({}))
+ )
Plotly.purge = sinon.stub()
+ }
+
+ const mockBrowser = () => {
document.createElement = sinon.stub()
+ window.decodeURIComponent = sinon.stub().returns('decoded image data')
+ window.encodeURIComponent = sinon.stub().returns('encoded image data')
+ }
+
+ const mockWindow = () => {
+ const win = createMockWindow()
+ sinon.stub(remote, 'createBrowserWindow').returns(win)
+ return {
+ win: win,
+ restore: () => remote.createBrowserWindow.restore()
+ }
}
t.test('v1.30.0 and up', t => {
- mock130()
+ t.test('(format png)', t => {
+ mock130()
- fn({}, {}, (errorCode, result) => {
- t.equal(result.imgData, 'image data')
- t.ok(Plotly.toImage.calledOnce)
- t.end()
+ fn({}, {}, (errorCode, result) => {
+ t.equal(result.imgData, 'image data')
+ t.ok(Plotly.toImage.calledOnce)
+ t.end()
+ })
})
+
+ t.test('(format pdf)', t => {
+ mock130()
+ mockBrowser()
+ const {win, restore} = mockWindow()
+ win.webContents.executeJavaScript.returns(new Promise(resolve => resolve()))
+ win.webContents.printToPDF.yields(null, 'pdf data')
+
+ fn({format: 'pdf'}, {}, (errorCode, result) => {
+ t.equal(errorCode, null)
+ t.equal(result.imgData, 'pdf data')
+ t.equal(window.encodeURIComponent.callCount, 1, 'encodeURIComponent calls')
+ t.equal(win.webContents.executeJavaScript.callCount, 1, 'executeJavaScript calls')
+ t.equal(win.webContents.printToPDF.callCount, 1, 'printToPDF calls')
+ t.ok(win.close.calledOnce)
+
+ restore()
+ t.end()
+ })
+ })
+
+ t.end()
})
t.test('v1.11.0 <= versions < v1.30.0', t => {
t.test('(format png)', t => {
mock110()
+ mockBrowser()
fn({}, {}, (errorCode, result) => {
t.equal(result.imgData, 'image data')
@@ -429,17 +496,41 @@ tap.test('render:', t => {
t.test('(format svg)', t => {
mock110()
- global.decodeURIComponent = sinon.stub().returns('decoded image data')
+ mockBrowser()
fn({format: 'svg'}, {}, (errorCode, result) => {
t.equal(result.imgData, 'decoded image data')
t.ok(document.createElement.calledOnce)
+ t.ok(window.decodeURIComponent.calledOnce)
t.ok(Plotly.newPlot.calledOnce)
t.ok(Plotly.toImage.calledOnce)
t.ok(Plotly.purge.calledOnce)
- t.ok(global.decodeURIComponent)
+ t.end()
+ })
+ })
- delete global.decodeURIComponent
+ t.test('(format pdf)', t => {
+ mock110()
+ mockBrowser()
+ const {win, restore} = mockWindow()
+ win.webContents.executeJavaScript.returns(new Promise(resolve => resolve()))
+ win.webContents.printToPDF.yields(null, 'pdf data')
+
+ fn({format: 'pdf'}, {}, (errorCode, result) => {
+ t.equal(errorCode, null)
+ t.equal(result.imgData, 'pdf data')
+ t.equal(document.createElement.callCount, 1, 'createElement calls')
+ t.equal(window.encodeURIComponent.callCount, 1, 'encodeURIComponent calls')
+ t.equal(win.webContents.executeJavaScript.callCount, 1, 'executeJavaScript calls')
+ t.equal(win.webContents.printToPDF.callCount, 1, 'printToPDF calls')
+ t.doesNotThrow(() => {
+ const gd = {
+ _fullLayout: {paper_bgcolor: 'some color'}
+ }
+ Plotly.toImage.args[0][1].setBackground(gd, 'some other color')
+ }, 'custom setBackground function')
+
+ restore()
t.end()
})
})
@@ -468,6 +559,41 @@ tap.test('render:', t => {
})
})
+ t.test('should return error code on executeJavaScript errors', t => {
+ mock130()
+ mockBrowser()
+ const {win, restore} = mockWindow()
+ win.webContents.executeJavaScript.returns(new Promise((resolve, reject) => {
+ reject(new Error('oh no!'))
+ }))
+
+ fn({format: 'pdf'}, {}, (errorCode, result) => {
+ t.equal(errorCode, 525)
+ t.match(result.error, /oh no/, 'error')
+ t.ok(win.close.calledOnce)
+
+ restore()
+ t.end()
+ })
+ })
+
+ t.test('should return error code on printToPDF errors', t => {
+ mock130()
+ mockBrowser()
+ const {win, restore} = mockWindow()
+ win.webContents.executeJavaScript.returns(new Promise(resolve => resolve()))
+ win.webContents.printToPDF.yields(new Error('oops'))
+
+ fn({format: 'pdf'}, {}, (errorCode, result) => {
+ t.equal(errorCode, 525)
+ t.match(result.error, /oops/, 'error')
+ t.ok(win.close.calledOnce)
+
+ restore()
+ t.end()
+ })
+ })
+
t.test('should generate svg for format pdf and eps', t => {
const formats = ['pdf', 'eps']