Skip to content

Commit cb90c36

Browse files
adamcarhedenMarkusBordihn
authored andcommitted
Added callback to get preview.executeScript result (#177)
* preview.executeScript() supports a callback that receives the return value of the code executed in the preview * Includes unit test for preview.executeScript() * Added messenger.cleanup() to stop listening for messages * Fixed preview.executeScript tests * Moved preview test from unit_tests to core_tests * Removed unnecessary token use in messenger * Removed debugging
1 parent 1cc5745 commit cb90c36

File tree

8 files changed

+328
-15
lines changed

8 files changed

+328
-15
lines changed

src/cache/cache.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ goog.require('cwc.utils.mime.getTypeByExtension');
3333
* @constructor
3434
* @struct
3535
* @final
36+
* @export
3637
*/
3738
cwc.Cache = function(helper) {
3839
/** @type {string} */
@@ -62,6 +63,7 @@ cwc.Cache = function(helper) {
6263

6364
/**
6465
* @async
66+
* @export
6567
*/
6668
cwc.Cache.prototype.prepare = async function() {
6769
await this.database_.open(this.databaseConfig_);
@@ -175,6 +177,7 @@ cwc.Cache.prototype.preloadFile = function(name) {
175177
/**
176178
* @param {!Array} files
177179
* @return {!Promise}
180+
* @export
178181
*/
179182
cwc.Cache.prototype.preloadFiles = function(files) {
180183
let promises = files.map(this.preloadFile.bind(this));

src/frameworks/internal/messenger/messenger.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,41 @@ cwc.framework.Messenger.prototype.postMessage = function(name, value) {
188188
* @private
189189
*/
190190
cwc.framework.Messenger.prototype.executeCode_ = function(code) {
191-
if (!code || typeof code !== 'string') {
192-
return;
193-
}
194-
// Remove trailing ";"" to avoid syntax errors for simple one liner
195-
if (code.endsWith(';')) {
196-
code = code.slice(0, -1);
197-
}
198-
// Skip the return parameter for more complex code
199-
if ((code.includes(';') && !code.includes('let')) || code.includes('{')) {
200-
console.log('>>' + Function(code)());
201-
} else {
202-
console.log('>>' + Function('return (' + code + ');')());
191+
let runCode = function(codeString) {
192+
// Skip the return parameter for more complex code
193+
let result = ((codeString.includes(';') && !codeString.includes('let')) ||
194+
codeString.includes('{')) ? Function(codeString)() :
195+
Function('return (' + codeString + ');')();
196+
console.log('>>' + result);
197+
return result;
198+
};
199+
200+
let result; // Declared here to apease jslint
201+
switch (typeof code) {
202+
case 'string':
203+
runCode(code);
204+
break;
205+
case 'object':
206+
if (!code.hasOwnProperty('code')) {
207+
console.error('Argument to executeCode_ missing property \'code\':',
208+
code);
209+
return;
210+
}
211+
if ((typeof code['code']) !== 'string') {
212+
console.error('\'code\' property of argument to executeCode_ is not a'
213+
+ ' string:', code);
214+
return;
215+
}
216+
result = runCode(code['code']);
217+
if (code.hasOwnProperty('id') && (typeof code['id']) === 'string') {
218+
this.send('__exec_result__', {
219+
'id': code['id'],
220+
'result': result,
221+
});
222+
}
223+
break;
224+
default:
225+
console.warn('Ignoring code due to unknown type', code);
203226
}
204227
};
205228

src/renderer/helper.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ goog.require('soydata');
2828
* @constructor
2929
* @struct
3030
* @final
31+
* @export
3132
*/
3233
cwc.renderer.Helper = function() {};
3334

@@ -209,6 +210,7 @@ cwc.renderer.Helper.prototype.getHTMLRunner = function(body, header,
209210
* @param {string} html
210211
* @param {string=} header
211212
* @return {string}
213+
* @export
212214
*/
213215
cwc.renderer.Helper.prototype.getRawHTML = function(html, header) {
214216
if (!html) {
@@ -397,6 +399,7 @@ cwc.renderer.Helper.prototype.getCacheFileHeader = function(name, cache) {
397399
* @param {!Array} names
398400
* @param {!cwc.Cache} cache
399401
* @return {string}
402+
* @export
400403
*/
401404
cwc.renderer.Helper.prototype.getCacheFilesHeader = function(names, cache) {
402405
let headers = '';

src/ui/preview/preview.js

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ goog.require('goog.dom.ViewportSizeMonitor');
3333
goog.require('goog.events.EventTarget');
3434
goog.require('goog.events.EventType');
3535
goog.require('goog.soy');
36+
goog.require('goog.string');
3637
goog.require('goog.ui.Component.EventType');
3738

3839

@@ -41,6 +42,7 @@ goog.require('goog.ui.Component.EventType');
4142
* @constructor
4243
* @struct
4344
* @final
45+
* @export
4446
*/
4547
cwc.ui.Preview = function(helper) {
4648
/** @type {string} */
@@ -102,6 +104,8 @@ cwc.ui.Preview = function(helper) {
102104

103105
/** @private {!cwc.Messenger} */
104106
this.messenger_ = new cwc.Messenger(this.eventHandler_);
107+
this.messenger_.addListener('__exec_result__', this.handleExecResponse_,
108+
this);
105109

106110
/** @private {string} */
107111
this.partition_ = 'preview';
@@ -111,12 +115,16 @@ cwc.ui.Preview = function(helper) {
111115

112116
/** @private {!cwc.utils.Logger|null} */
113117
this.log_ = new cwc.utils.Logger(this.name);
118+
119+
/** @private {!Object<string, Function>} */
120+
this.pendingExecCallbacks = {};
114121
};
115122

116123

117124
/**
118125
* Decorates the given node and adds the preview window.
119126
* @param {Element=} node The target node to add the preview window.
127+
* @export
120128
*/
121129
cwc.ui.Preview.prototype.decorate = function(node) {
122130
this.node = node || goog.dom.getElement(this.prefix + 'chrome');
@@ -345,6 +353,7 @@ cwc.ui.Preview.prototype.terminate = function() {
345353

346354
/**
347355
* @return {Object}
356+
* @export
348357
*/
349358
cwc.ui.Preview.prototype.getContent = function() {
350359
return this.content;
@@ -367,6 +376,7 @@ cwc.ui.Preview.prototype.getContentUrl = function() {
367376

368377
/**
369378
* @param {string} url
379+
* @export
370380
*/
371381
cwc.ui.Preview.prototype.setContentUrl = function(url) {
372382
if (!url || !this.content) {
@@ -463,11 +473,83 @@ cwc.ui.Preview.prototype.focus = function() {
463473
/**
464474
* Injects and executes the passed code in the preview content, if supported.
465475
* @param {!(string|Function)} code
476+
* @param {Number} timeout
477+
* @return {!Promise}
478+
* @export
479+
* @TODO([email protected]): Move logic to messenger instance.
466480
*/
467-
cwc.ui.Preview.prototype.executeScript = function(code) {
481+
cwc.ui.Preview.prototype.executeScript = function(code, timeout = 250) {
468482
this.log_.info('Execute script', code);
469-
this.messenger_.send('__exec__',
470-
typeof code === 'function' ? code.toString() : code);
483+
let execSpec = {
484+
'code': typeof code === 'function' ? code.toString() : code,
485+
'id': false,
486+
};
487+
let id = goog.string.createUniqueString();
488+
let promise = new Promise(function(resolve, reject) {
489+
this.pendingExecCallbacks[id] = resolve;
490+
setTimeout(function() {
491+
if (this.pendingExecCallbacks.hasOwnProperty(id)) {
492+
this.pendingExecCallbacks[id] = false;
493+
reject(`Preview script timed out after ${timeout}ms`);
494+
}
495+
}.bind(this), timeout);
496+
}.bind(this));
497+
execSpec['id'] = id;
498+
this.messenger_.send('__exec__', execSpec);
499+
return promise;
500+
};
501+
502+
/**
503+
* Calls the callback registered for the script execution
504+
* @param {Object} response
505+
* @private
506+
* @TODO([email protected]): Move logic to messenger instance.
507+
*/
508+
cwc.ui.Preview.prototype.handleExecResponse_ = function(response) {
509+
if ((typeof response) !== 'object') {
510+
this.log_.warn('Received non-object as response from script execution',
511+
response);
512+
return;
513+
}
514+
if (!response.hasOwnProperty('id')) {
515+
this.log_.warn('executeScript response has no \'id\', can\'t match to a '+
516+
'callback');
517+
return;
518+
}
519+
if ((typeof response['id']) !== 'string') {
520+
this.log_.warn('Ignorning executeScript response with non-string id',
521+
response);
522+
return;
523+
}
524+
// The callback is called from setTimeout() to give the executeScript timeout
525+
// a chance to run first. When run in a WebView, the preview runs in a
526+
// separate thread and the executeScript timeout will have already triggered
527+
// if the user's functon exceeded the timeout. However, iframes run in the
528+
// same thread, so a long-running script will prevent the executeScript
529+
// timeout from firing until the it completes.
530+
// This does make timeouts ineffective for iframes, but ensuring the correct
531+
// order allows test logic to execute correctly in karma/chrome (which only
532+
// supports iframes).
533+
setTimeout(() => {
534+
if (!this.pendingExecCallbacks.hasOwnProperty(response['id'])) {
535+
this.log_.warn('Callback for executeScript response', response['id'],
536+
'missing. Response was', response);
537+
return;
538+
}
539+
let callback = this.pendingExecCallbacks[response['id']];
540+
delete this.pendingExecCallbacks[response['id']];
541+
if (callback === false) {
542+
this.log_.info('executeScript ', response['id'], 'timed out');
543+
return;
544+
}
545+
let result;
546+
if (response.hasOwnProperty('result')) {
547+
result = response['result'];
548+
}
549+
this.log_.info('Executing callback ', response['id'], 'with result',
550+
result);
551+
callback(result);
552+
}, 0);
471553
};
472554

473555

test/.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
'globals': {
3+
'afterAll': true,
34
'afterEach': true,
45
'beforeAll': true,
56
'beforeEach': true,

test/core_tests/.eslintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
'rules': {
3+
'no-unused-vars': [2, {
4+
'varsIgnorePattern': '^i18soy$'
5+
}]
6+
}
7+
}

0 commit comments

Comments
 (0)