Permalink
Browse files

High-fidelity screenshots through browser extension.

  • Loading branch information...
tamasd committed Sep 6, 2016
1 parent b49de50 commit 2b2363561adc912586e94909d1cb680273f94ace
View
@@ -16,6 +16,7 @@
"menuitems": "",
"pwauth": true,
"extrabuild": "",
"extensionid": "",
"google": {
"id": "",
"secret": ""
View
@@ -199,7 +199,7 @@ function onMessageEventHandler(event) {
$(".walkthroughbutton")
.filter(function() { return $(this).data("origin") === WALKHUB_URL; })
.html("");
window.removeEventListener(onMessageEventHandler);
window.removeEventListener("message", onMessageEventHandler);
return;
}
@@ -82,8 +82,7 @@ class Controller {
if (that.walkthrough.steps[that.state.stepIndex] && !that.state.completed) {
that.client.log("Loading step");
that.refreshStep();
}
else {
} else {
that.client.log("Empty step");
that.nextStep();
}
@@ -159,7 +158,7 @@ class Controller {
});
if (CommandDispatcher.instance().isAutomaticCommand(this.step.pureCommand)) {
this.client.log("Automatically executing step.");
this.nextStep();
this.nextStep(true);
}
}
@@ -185,10 +184,10 @@ class Controller {
});
}
nextStep() {
nextStep(skip_screenshot = false) {
var that = this;
this.maybeScreenshot(() => {
const after = () => {
Bubble.current && Bubble.current.hide();
if (!this.state.completed && this.step) {
this.client.log("Executing incomplete step.");
@@ -236,7 +235,13 @@ class Controller {
this.state.completed = false;
this.client.updateState(this.state);
this.refreshStep();
});
};
if (skip_screenshot) {
after();
} else {
this.maybeScreenshot(after);
}
}
updateCurrentStep(step, callback) {
@@ -319,39 +324,102 @@ class Controller {
return tmpcanvas;
}
maybeScreenshot(after) {
if (this.state.screening) {
html2canvas(document.body, {
onrendered: (canvas) => {
if (!this.state.screensize) {
this.state.screensize = {
width: window.innerWidth,
height: window.innerHeight,
};
this.client.updateState(this.state);
}
if (canvas.height != this.state.screensize.height || canvas.width != this.state.screensize.width) {
canvas = this.resizeCanvas(canvas);
}
cropImage(dataUrl, crop, after) {
if (!crop) {
if (after) {
after(dataUrl);
}
return;
}
const data = canvas.toDataURL("image/png");
this.client.sendScreenshot(data);
const img = new Image;
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = crop.width;
canvas.height = crop.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, crop.left * crop.ratio, crop.top * crop.ratio, crop.width * crop.ratio, crop.height * crop.ratio, 0, 0, canvas.width, canvas.height);
if (after) {
after(canvas.toDataURL("image/png"));
}
};
img.src = dataUrl;
}
if (after) {
after();
}
},
background: undefined,
letterRendering: true,
})
maybeScreenshot(after) {
if (this.state.screening) {
if (this.chromeExtensionScreenshot(after)) {
return;
}
if (this.html2canvasScreenshot(after)) {
return;
}
} else {
if (after) {
after();
}
}
}
chromeExtensionScreenshot(after) {
if (window.WALKHUB_EXTENSION) {
const key = Math.random().toString();
const handler = (event) => {
const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
if (data && data.screenshot_key === key && data.command === "saveScreenshot") {
this.cropImage(data.img, this.state.screenshotCrop, (image) => {
this.client.sendScreenshot(image);
if (after) {
after();
}
});
window.removeEventListener("message", handler);
}
};
window.addEventListener("message", handler);
window.postMessage(JSON.stringify({
command: "makeScreenshot",
walkhub_extension_key: EXTENSIONID,
screenshot_key: key,
origin: window.location.origin,
}), "*");
return true;
}
return false;
}
html2canvasScreenshot(after) {
html2canvas(document.body, {
onrendered: (canvas) => {
if (!this.state.screensize) {
this.state.screensize = {
width: window.innerWidth,
height: window.innerHeight,
};
this.client.updateState(this.state);
}
if (canvas.height != this.state.screensize.height || canvas.width != this.state.screensize.width) {
canvas = this.resizeCanvas(canvas);
}
const data = canvas.toDataURL("image/png");
this.client.sendScreenshot(data);
if (after) {
after();
}
},
background: undefined,
letterRendering: true,
});
return true;
}
}
export default Controller;
@@ -140,7 +140,8 @@ class Executor {
}));
}
error = true;
this.logger.logResult(this.controller.state, false, "locator-fail: [locator]".replace("[locator]", step.highlight));
this.logger.logResult(this.controller.state, false,
"locator-fail: [locator]".replace("[locator]", step.highlight));
},
giveUp: noElement
});
@@ -150,7 +151,8 @@ class Executor {
} else {
this.client.showError("command-not-supported",
"The command '[command]' is not supported.".replace("[command]", command));
this.logger.logResult(this.controller.state, false, "command-not-supported: [command]".replace("[command]", command));
this.logger.logResult(this.controller.state, false,
"command-not-supported: [command]".replace("[command]", command));
}
}, 0);
}
View
@@ -34,7 +34,7 @@ class Modal extends React.Component {
height: `${this.state.height}px`,
};
return (
<div className={`wh-modal ${this.props.className}`} style={style}>{this.props.children || " "}</div>
<div ref="modal" className={`wh-modal ${this.props.className}`} style={style}>{this.props.children || " "}</div>
);
}
@@ -130,7 +130,7 @@ class WalkhubIframe extends React.Component {
</Bar>
<div className="container-fluid iframe-container">
<div className="row">
<iframe ref="contentIframe" style={{height: `${this.state.height}px`}} className="col-xs-12" src={this.props.src} frameBorder="0" scrolling="auto" allowTransparency="true"></iframe>
<iframe id="walkhub-iframe" ref="contentIframe" style={{height: `${this.state.height}px`}} className="col-xs-12" src={this.props.src} frameBorder="0" scrolling="auto" allowTransparency="true"></iframe>
</div>
</div>
</Modal>
View
@@ -39,6 +39,10 @@ class Runner {
getName() {
return "";
}
screenshotCrop() {
return false;
}
}
export default Runner;
@@ -18,6 +18,7 @@ import React from "react";
import Runner from "walkthrough/runner";
import WalkhubIframe from "components/walkhub_iframe";
import {t} from "t";
import $ from "jquery";
class IframeRunner extends Runner {
reloadHTTP = true;
@@ -39,6 +40,19 @@ class IframeRunner extends Runner {
getName() {
return "iframe";
}
screenshotCrop() {
const elem = $("iframe#walkhub-iframe");
if (elem.length === 1) {
return Object.assign({
width: elem.width(),
height: elem.height(),
ratio: (window.devicePixelRatio || 1),
}, elem.offset());
}
return false;
}
}
export default IframeRunner;
@@ -291,7 +291,9 @@ class WalkhubBackend {
handleGetState(data, source) {
this.post(this.maybeProxy({
type: "state",
state: this.state,
state: Object.assign({
screenshotCrop: this.runner.screenshotCrop(),
}, this.state),
}, data), source);
}
View
@@ -181,6 +181,10 @@ func screeningService(ec *ab.EntityController) ab.Service {
images[fn] = dataurl.Data
}
if len(images) == 0 {
ab.Fail(http.StatusBadRequest, errors.New("no images sent"))
}
for name, content := range images {
if err := ioutil.WriteFile(name, content, 0644); err != nil {
ab.LogUser(r).Println(err)
@@ -226,6 +230,15 @@ func screeningService(ec *ab.EntityController) ab.Service {
return
}
// Short-circuits the gif generation process.
// If the client wants JSON, there is no need
// to go through the GIF generation, which is
// an expensive process.
if r.Header.Get("Accept") == "application/json" {
reply()
return
}
mtx.Lock()
l, ok := lock[fn]
if ok {
@@ -251,7 +264,13 @@ func screeningService(ec *ab.EntityController) ab.Service {
mtx.Unlock()
}()
ab.MaybeFail(http.StatusInternalServerError, err)
if err != nil {
if _, ok := err.(*os.PathError); ok {
ab.Fail(http.StatusNotFound, err)
} else {
ab.Fail(http.StatusInternalServerError, err)
}
}
close(l)
reply()
}))
View
@@ -14,6 +14,7 @@ var menuitems = process.env.MENUITEMS || serverConfig.menuitems;
var frontpagecomponent = process.env.FRONTPAGECOMPONENT || serverConfig.frontpagecomponent;
var footercomponent = process.env.FOOTERCOMPONENT || serverConfig.footercomponent;
var baseurl = process.env.BASEURL || serverConfig.baseurl;
var extensionid = process.env.EXTENSIONID || serverConfig.extensionid;
var embedurl = process.env.EMBEDURL || serverConfig.embedurl;
var httporigin = process.env.HTTPORIGIN || serverConfig.httporigin;
var gaAccount = process.env.GOOGLEANALYTICSACCOUNT || serverConfig.googleanalyticsaccount;
@@ -128,6 +129,7 @@ module.exports = {
WALKHUB_URL: JSON.stringify(baseurl),
WALKHUB_EMBED_URL: JSON.stringify(embedurl ? embedurl : baseurl),
WALKHUB_HTTP_URL: JSON.stringify(httporigin ? httporigin : baseurl),
EXTENSIONID: JSON.stringify(extensionid),
WALKHUB_MENU_ITEMS: !!menuitems,
WALKHUB_CONTENT_PAGES: !!contentpages,
GA_ACCOUNT: JSON.stringify(gaAccount),

0 comments on commit 2b23635

Please sign in to comment.