mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Screen snippet (#55)
* adds screen snippet feature * update return api * udpate unit tests
This commit is contained in:
@@ -40,6 +40,12 @@
|
||||
<button id='inc-badge'>increment badge count</button>
|
||||
<br>
|
||||
<button id='clear-badge'>clear badge count</button>
|
||||
<br>
|
||||
<hr>
|
||||
<p>Screen Snippet (note: currently only works on mac):</p>
|
||||
<button id='snippet'>get snippet</button>
|
||||
<p>snippet output:</p>
|
||||
<image id='snippet-img'/>
|
||||
</body>
|
||||
<script>
|
||||
var notfEl = document.getElementById('notf');
|
||||
@@ -100,5 +106,27 @@
|
||||
SYM_API.setBadgeCount(0);
|
||||
});
|
||||
|
||||
var snippetButton = document.getElementById('snippet');
|
||||
snippetButton.addEventListener('click', function() {
|
||||
let snippet = new SYM_API.ScreenSnippet();
|
||||
|
||||
snippet
|
||||
.capture()
|
||||
.then(gotSnippet)
|
||||
.catch(snippetError);
|
||||
|
||||
function gotSnippet(rsp) {
|
||||
if (rsp && rsp.data && rsp.type) {
|
||||
var dataUrl = 'data:' + rsp.type + ',' + rsp.data;
|
||||
var img = document.getElementById('snippet-img');
|
||||
img.src = dataUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function snippetError(err) {
|
||||
alert('error getting snippet', err);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
@@ -25,6 +25,7 @@ const local = {
|
||||
};
|
||||
|
||||
var notify = remote.require('./notify/notifyImpl.js');
|
||||
var ScreenSnippet = remote.require('./screenSnippet/ScreenSnippet.js');
|
||||
|
||||
// throttle calls to this func to at most once per sec, called on leading edge.
|
||||
const throttledSetBadgeCount = throttle(1000, function(count) {
|
||||
@@ -65,6 +66,12 @@ window.SYM_API = {
|
||||
*/
|
||||
Notification: notify,
|
||||
|
||||
/**
|
||||
* provides api to allow user to capture portion of screen, see api
|
||||
* details in screenSnipper/ScreenSnippet.js
|
||||
*/
|
||||
ScreenSnippet: ScreenSnippet,
|
||||
|
||||
/**
|
||||
* allows JS to register a logger that can be used by electron main process.
|
||||
* @param {Object} logger function that can be called accepting
|
||||
|
141
js/screenSnippet/ScreenSnippet.js
Normal file
141
js/screenSnippet/ScreenSnippet.js
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const childProcess = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { isMac } = require('../utils/misc.js');
|
||||
|
||||
// static ref to child process, only allow one screen snippet at time, so
|
||||
// hold ref to prev, so can kill before starting next snippet.
|
||||
let child;
|
||||
|
||||
/**
|
||||
* Captures a user selected portion of the monitor and returns jpeg image
|
||||
* encoded in base64 format.
|
||||
*/
|
||||
class ScreenSnippet {
|
||||
/**
|
||||
* Returns promise.
|
||||
*
|
||||
* If successful will resolves with jpeg image encoded in base64 format:
|
||||
* {
|
||||
* type: 'image/jpg;base64',
|
||||
* data: base64-data
|
||||
* }
|
||||
*
|
||||
* Otherwise if not successful will reject with object
|
||||
* containing: { type: ['WARN','ERROR'], message: String }
|
||||
*/
|
||||
capture() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let captureUtil, captureUtilArgs;
|
||||
|
||||
let tmpFilename = 'symphonyImage-' + Date.now() + '.jpg';
|
||||
let tmpDir = os.tmpdir();
|
||||
|
||||
let outputFileName = path.join(tmpDir, tmpFilename);
|
||||
|
||||
if (isMac) {
|
||||
// utilize Mac OSX built-in screencapture tool which has been
|
||||
// available since OSX ver 10.2.
|
||||
captureUtil = '/usr/sbin/screencapture';
|
||||
captureUtilArgs = [ '-i', '-s', '-t', 'jpg', outputFileName ];
|
||||
} else {
|
||||
// for now screen snippet on windows is not supported,
|
||||
// waiting on open sourcing for custom screen snippter util.
|
||||
reject(this._createWarn('windows not supported'));
|
||||
// custom built windows screen capture tool, that is
|
||||
// located in same directory as running exec.
|
||||
//captureUtil = './ScreenSnippet.exe';
|
||||
//captureUtilArgs = [ outputFileName ];
|
||||
}
|
||||
|
||||
// only allow one screen capture at a time.
|
||||
if (child) {
|
||||
child.kill();
|
||||
}
|
||||
|
||||
child = childProcess.execFile(captureUtil, captureUtilArgs, (error) => {
|
||||
// will be called when child process exits.
|
||||
if (error && error.killed) {
|
||||
// processs was killed, just resolve with no data.
|
||||
resolve();
|
||||
} else {
|
||||
this._readResult(outputFileName, resolve, reject, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// private methods below here
|
||||
|
||||
_readResult(outputFileName, resolve, reject, childProcessErr) {
|
||||
fs.readFile(outputFileName, (readErr, data) => {
|
||||
if (readErr) {
|
||||
let returnErr;
|
||||
if (readErr.code === 'ENOENT') {
|
||||
// no such file exists, user likely aborted
|
||||
// creating snippet. also include any error when
|
||||
// creating child process.
|
||||
returnErr = this._createWarn('file does not exist ' +
|
||||
childProcessErr);
|
||||
} else {
|
||||
returnErr = this._createError(readErr + ',' +
|
||||
childProcessErr);
|
||||
}
|
||||
|
||||
reject(returnErr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
reject(this._createWarn('no file data provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// convert binary data to base64 encoded string
|
||||
let output = Buffer(data).toString('base64');
|
||||
resolve({
|
||||
type: 'image/jpg;base64',
|
||||
data: output
|
||||
});
|
||||
} catch (error) {
|
||||
reject(this._createError(error));
|
||||
}
|
||||
finally {
|
||||
// remove tmp file (async)
|
||||
fs.unlink(outputFileName, function(removeErr) {
|
||||
// note: node complains if calling async
|
||||
// func without callback.
|
||||
/* eslint-disable no-console */
|
||||
if (removeErr) {
|
||||
console.error(
|
||||
'error removing temp snippet file: ' +
|
||||
outputFileName + ', err:' + removeErr);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
_createError(msg) {
|
||||
var err = new Error(msg);
|
||||
err.type = 'ERROR';
|
||||
return err;
|
||||
}
|
||||
|
||||
_createWarn(msg) {
|
||||
var err = new Error(msg);
|
||||
err.type = 'WARN';
|
||||
return err;
|
||||
}
|
||||
/* eslint-enable class-methods-use-this */
|
||||
}
|
||||
|
||||
module.exports = ScreenSnippet;
|
128
tests/ScreenSnippet.test.js
Normal file
128
tests/ScreenSnippet.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
|
||||
const ScreenSnippet = require('../js/screenSnippet/ScreenSnippet.js');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
const snippetBase64 = require('./fixtures/snippet/snippet-base64.js');
|
||||
|
||||
// mock child_process used in ScreenSnippet
|
||||
jest.mock('child_process', function() {
|
||||
return {
|
||||
execFile: mockedExecFile
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* mock version of execFile just creates a copy of a test jpeg file.
|
||||
*/
|
||||
function mockedExecFile(util, args, doneCallback) {
|
||||
let outputFileName = args[args.length - 1];
|
||||
|
||||
copyTestFile(outputFileName, function(copyTestFile) {
|
||||
doneCallback();
|
||||
});
|
||||
}
|
||||
|
||||
function copyTestFile(destFile, done) {
|
||||
const testfile = path.join(__dirname ,
|
||||
'fixtures/snippet/ScreenSnippet.jpeg');
|
||||
|
||||
let reader = fs.createReadStream(testfile);
|
||||
let writer = fs.createWriteStream(destFile);
|
||||
|
||||
writer.on('close', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
reader.pipe(writer);
|
||||
}
|
||||
|
||||
function createTestFile(done) {
|
||||
let tmpDir = os.tmpdir();
|
||||
const testFileName = path.join(tmpDir,
|
||||
'ScreenSnippet-' + Date.now() + '.jpeg');
|
||||
|
||||
copyTestFile(testFileName, function() {
|
||||
done(testFileName)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
describe('Tests for ScreenSnippet', function() {
|
||||
describe('when reading a valid jpeg file', function() {
|
||||
it('should match base64 output', function(done) {
|
||||
let s = new ScreenSnippet();
|
||||
s.capture().then(gotImage);
|
||||
|
||||
function gotImage(rsp) {
|
||||
expect(rsp.type).toEqual('image/jpg;base64');
|
||||
expect(rsp.data).toEqual(snippetBase64);
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it('should remove output file after completed', function(done) {
|
||||
createTestFile(function(testfileName) {
|
||||
let s = new ScreenSnippet();
|
||||
s._readResult(testfileName, resolve);
|
||||
|
||||
function resolve() {
|
||||
// should be long enough before file
|
||||
// gets removed
|
||||
setTimeout(function() {
|
||||
let exists = fs.existsSync(testfileName);
|
||||
expect(exists).toBe(false);
|
||||
done();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if output file does not exist', function(done) {
|
||||
let s = new ScreenSnippet();
|
||||
let nonExistentFile = 'bogus.jpeg'
|
||||
s._readResult(nonExistentFile, resolve, reject);
|
||||
|
||||
function resolve() {
|
||||
// shouldn't get here
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
|
||||
function reject(err) {
|
||||
expect(err).toBeTruthy();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if read file fails', function(done) {
|
||||
var origFsReadFile = fs.readFile;
|
||||
|
||||
fs.readFile = jest.fn(mockedReadFile);
|
||||
|
||||
function mockedReadFile(filename, callback) {
|
||||
callback(new Error('failed'));
|
||||
}
|
||||
|
||||
let s = new ScreenSnippet();
|
||||
s.capture().then(resolved).catch(rejected);
|
||||
|
||||
function resolved(err) {
|
||||
cleanup();
|
||||
// shouldn't get here
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
|
||||
function rejected(err) {
|
||||
expect(err).toBeTruthy();
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
fs.readFile = origFsReadFile;
|
||||
}
|
||||
});
|
||||
});
|
BIN
tests/fixtures/snippet/ScreenSnippet.jpeg
vendored
Normal file
BIN
tests/fixtures/snippet/ScreenSnippet.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
5
tests/fixtures/snippet/snippet-base64.js
vendored
Normal file
5
tests/fixtures/snippet/snippet-base64.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// base64 conversion of file ScreenSnippet.jpeg
|
||||
const base64ScreenSnippet =
|
||||
"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"
|
||||
|
||||
module.exports = base64ScreenSnippet
|
Reference in New Issue
Block a user