Screen snippet (#55)

* adds screen snippet feature

* update return api

* udpate unit tests
This commit is contained in:
Lynn
2017-04-11 11:58:35 -07:00
committed by GitHub
parent 1e6dfec68c
commit 6b99dc541e
6 changed files with 309 additions and 0 deletions

View File

@@ -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>

View File

@@ -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

View 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
View 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;
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,5 @@
// base64 conversion of file ScreenSnippet.jpeg
const base64ScreenSnippet =
"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"
module.exports = base64ScreenSnippet