summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Greenberg <ifnspifn@gmail.com>2013-09-24 16:17:32 -0700
committerWill Greenberg <ifnspifn@gmail.com>2013-09-24 16:17:32 -0700
commit8ceb8ac14c85c812308b9d93e29f76e985e7b688 (patch)
tree249a07deff03112680498c6942e6a6fd74a8b065
parentf6df19cad1581dc29f62c6a7d136fc77ecb997b9 (diff)
parent4cbd3c6c4620797c65b3e9507529907122e1f306 (diff)
Merge pull request #312 from endlessm/issues/310
Issues/310
-rw-r--r--test/webhelper/smoke-tests/webview.js135
-rw-r--r--test/webhelper/testTranslate.js39
-rw-r--r--test/webhelper/testWebActions.js98
-rw-r--r--webhelper/webhelper.js133
4 files changed, 265 insertions, 140 deletions
diff --git a/test/webhelper/smoke-tests/webview.js b/test/webhelper/smoke-tests/webview.js
index fe2a655..a3b91e5 100644
--- a/test/webhelper/smoke-tests/webview.js
+++ b/test/webhelper/smoke-tests/webview.js
@@ -1,13 +1,11 @@
//Copyright 2013 Endless Mobile, Inc.
-const Lang = imports.lang;
const Endless = imports.gi.Endless;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
-const WebKit = imports.gi.WebKit;
-
-// WebHelper.js must be copied somewhere in GJS's imports.searchPath
+const Lang = imports.lang;
const WebHelper = imports.webhelper;
+const WebKit = imports.gi.WebKit;
const TEST_APPLICATION_ID = 'com.endlessm.example.test-webview';
@@ -62,51 +60,46 @@ const TestApplication = new Lang.Class({
Name: 'TestApplication',
Extends: WebHelper.Application,
- _translationFunction: function(string) {
- return string.italics();
+ /* *** ACTIONS AVAILABLE FROM THE WEB VIEW *** */
+
+ /* dict['name'] is the name of the page to move to */
+ moveToPage: function(dict) {
+ this._pm.visible_page_name = dict['name'];
},
- /* *** ACTIONS AVAILABLE FROM THE WEB VIEW *** */
+ /* dict['msg'] is the message to display */
+ showMessageFromParameter: function(dict) {
+ let dialog = new Gtk.MessageDialog({
+ buttons: Gtk.ButtonsType.CLOSE,
+ message_type: Gtk.MessageType.INFO,
+ text: dict['msg']
+ });
+ dialog.set_transient_for(this._window);
+ dialog.run();
+ dialog.destroy();
+ },
+
+ /* dict['id'] is the ID of the input field to use */
+ showMessageFromInputField: function(dict) {
+ let input = this._getElementById(this._webview, dict['id']);
+
+ // WebKitDOMHTMLInputElement
+ let msg = input.get_value();
+
+ let dialog = new Gtk.MessageDialog({
+ buttons: Gtk.ButtonsType.CLOSE,
+ message_type: Gtk.MessageType.INFO,
+ text: msg
+ });
+ dialog.set_transient_for(this._window);
+ dialog.run();
+ dialog.destroy();
+ },
- _webActions: {
- /* dict['name'] is the name of the page to move to */
- 'moveToPage': function(dict) {
- print('move to page '+dict['name']);
- this._pm.visible_page_name = dict['name'];
- },
-
- /* dict['msg'] is the message to display */
- 'showMessageFromParameter': function(dict) {
- let dialog = new Gtk.MessageDialog({
- buttons: Gtk.ButtonsType.CLOSE,
- message_type: Gtk.MessageType.INFO,
- text: dict['msg'] });
- dialog.set_transient_for(this._window);
- dialog.run();
- dialog.destroy();
- },
-
- /* dict['id'] is the ID of the input field to use */
- 'showMessageFromInputField': function(dict) {
- let input = this._getElementById(this._webview, dict['id']);
-
- // WebKitDOMHTMLInputElement
- let msg = input.get_value();
-
- let dialog = new Gtk.MessageDialog({
- buttons: Gtk.ButtonsType.CLOSE,
- message_type: Gtk.MessageType.INFO,
- text: msg });
- dialog.set_transient_for(this._window);
- dialog.run();
- dialog.destroy();
- },
-
- /* dict['id'] is the ID of the element to use */
- 'addStars': function(dict) {
- let e = this._getElementById(this._webview, dict['id']);
- e.inner_text += '★ ';
- }
+ /* dict['id'] is the ID of the element to use */
+ addStars: function(dict) {
+ let e = this._getElementById(this._webview, dict['id']);
+ e.inner_text += '★ ';
},
/* *************************** */
@@ -114,46 +107,54 @@ const TestApplication = new Lang.Class({
vfunc_startup: function() {
this.parent();
+ this.set_translation_function(function(string) {
+ return string.italics();
+ });
+ this.define_web_actions({
+ moveToPage: this.moveToPage,
+ showMessageFromParameter: this.showMessageFromParameter,
+ showMessageFromInputField: this.showMessageFromInputField,
+ addStars: this.addStars
+ });
+
this._webview = new WebKit.WebView();
this._webview.load_string(TEST_HTML, 'text/html', 'UTF-8', 'file://');
this._webview.connect('notify::load-status',
- Lang.bind(this, function (web_view, status) {
- if (web_view.load_status == WebKit.LoadStatus.FINISHED) {
- // now we translate to Brazilian Portuguese
- this.translate_html(web_view);
- }
- }));
+ Lang.bind(this, function (webview) {
+ if (webview.load_status == WebKit.LoadStatus.FINISHED)
+ this.translate_html(webview);
+ }));
- this._webview.connect('navigation-policy-decision-requested',
- Lang.bind(this, this.web_actions_handler));
+ this._webview.connect('navigation-policy-decision-requested',
+ Lang.bind(this, this.web_actions_handler));
this._page1 = new Gtk.ScrolledWindow();
this._page1.add(this._webview);
this._page2 = new Gtk.Grid();
- let back_button = new Gtk.Button({label:"Go back to page 1"});
+ let back_button = new Gtk.Button({ label:"Go back to page 1" });
back_button.connect('clicked', Lang.bind(this, function() {
this._pm.visible_page_name = 'page1';
- }))
+ }));
this._page2.add(back_button);
- this._pm = new Endless.PageManager();
- this._pm.set_transition_type(Endless.PageManagerTransitionType.CROSSFADE)
- this._pm.add(this._page1, { name: 'page1' });
- this._pm.add(this._page2, { name: 'page2' });
-
- this._pm.visible_page = this._page1;
-
this._window = new Endless.Window({
application: this,
- border_width: 16,
- page_manager: this._pm
+ border_width: 16
});
+ this._pm = this._window.page_manager;
+ this._pm.set_transition_type(Endless.PageManagerTransitionType.CROSSFADE);
+ this._pm.add(this._page1, { name: 'page1' });
+ this._pm.add(this._page2, { name: 'page2' });
+ this._pm.visible_page = this._page1;
+
this._window.show_all();
}
});
-let app = new TestApplication({ application_id: TEST_APPLICATION_ID,
- flags: 0 });
+let app = new TestApplication({
+ application_id: TEST_APPLICATION_ID,
+ flags: 0
+});
app.run(ARGV);
diff --git a/test/webhelper/testTranslate.js b/test/webhelper/testTranslate.js
index 10e389c..651ddf5 100644
--- a/test/webhelper/testTranslate.js
+++ b/test/webhelper/testTranslate.js
@@ -49,24 +49,47 @@ function setUp() {
function testStringIsTranslated() {
let translationFunctionWasCalled = false;
let translationFunctionCalledWithString;
- app._translationFunction = function(s) {
+ app.set_translation_function(function(s) {
translationFunctionWasCalled = true;
translationFunctionCalledWithString = s;
return s;
- };
+ });
app.run([]);
assertTrue(translationFunctionWasCalled);
assertEquals('Translate Me', translationFunctionCalledWithString);
}
-// The following two tests are commented out because GJS cannot catch exceptions
+// The following test is commented out because GJS cannot catch exceptions
// across FFI interfaces (e.g. in GObject callbacks.)
// function testMissingTranslationFunctionIsHandled() {
-// assertRaises(app.run([]));
+// assertRaises(function() {
+// app.run([]);
+// });
// }
-// function testBadTranslationFunctionIsHandled() {
-// app._translationFunction = "I am not a function";
-// assertRaises(app.run([]));
-// } \ No newline at end of file
+function testSetBadTranslationFunction() {
+ assertRaises(function() {
+ app.set_translation_function("I am not a function");
+ });
+}
+
+function testGetSetTranslationFunction() {
+ let translationFunction = function(string) {
+ return string;
+ };
+ app.set_translation_function(translationFunction);
+ let actualTranslationFunction = app.get_translation_function();
+ assertEquals(translationFunction, actualTranslationFunction);
+}
+
+function testTranslationFunctionIsNullByDefault() {
+ assertNull(app.get_translation_function());
+}
+
+function testGetSetNullTranslationFunction() {
+ app.set_translation_function(function (s) { return s; });
+ assertNotNull(app.get_translation_function());
+ app.set_translation_function(null);
+ assertNull(app.get_translation_function());
+}
diff --git a/test/webhelper/testWebActions.js b/test/webhelper/testWebActions.js
index 585ab00..0ecc235 100644
--- a/test/webhelper/testWebActions.js
+++ b/test/webhelper/testWebActions.js
@@ -45,12 +45,10 @@ function setUp() {
function testWebActionIsCalled() {
let actionWasCalled = false;
- app._webActions = {
- quitApplication: function() {
- actionWasCalled = true;
- app.quit();
- }
- };
+ app.define_web_action('quitApplication', function() {
+ actionWasCalled = true;
+ app.quit();
+ });
app.webActionToTest = 'endless://quitApplication';
app.run([]);
assertTrue(actionWasCalled);
@@ -58,12 +56,10 @@ function testWebActionIsCalled() {
function testWebActionIsCalledWithParameter() {
let actionParameter;
- app._webActions = {
- getParameterAndQuit: function(dict) {
- actionParameter = dict['param'];
- app.quit();
- }
- };
+ app.define_web_action('getParameterAndQuit', function(dict) {
+ actionParameter = dict['param'];
+ app.quit();
+ });
app.webActionToTest = 'endless://getParameterAndQuit?param=value';
app.run([]);
assertEquals('value', actionParameter);
@@ -71,14 +67,12 @@ function testWebActionIsCalledWithParameter() {
function testWebActionIsCalledWithManyParameters() {
let firstParameter, secondParameter, thirdParameter;
- app._webActions = {
- getParametersAndQuit: function(dict) {
- firstParameter = dict['first'];
- secondParameter = dict['second'];
- thirdParameter = dict['third'];
- app.quit();
- }
- };
+ app.define_web_action('getParametersAndQuit', function(dict) {
+ firstParameter = dict['first'];
+ secondParameter = dict['second'];
+ thirdParameter = dict['third'];
+ app.quit();
+ });
app.webActionToTest = 'endless://getParametersAndQuit?first=thefirst&second=thesecond&third=thethird';
app.run([]);
assertEquals('thefirst', firstParameter);
@@ -89,12 +83,10 @@ function testWebActionIsCalledWithManyParameters() {
function testParameterNameIsUriDecoded() {
let expectedParameter = 'päräm💩';
let parameterWasFound = false;
- app._webActions = {
- getUriDecodedParameterAndQuit: function(dict) {
- parameterWasFound = (expectedParameter in dict);
- app.quit();
- }
- };
+ app.define_web_action('getUriDecodedParameterAndQuit', function(dict) {
+ parameterWasFound = (expectedParameter in dict);
+ app.quit();
+ });
app.webActionToTest = 'endless://getUriDecodedParameterAndQuit?p%C3%A4r%C3%A4m%F0%9F%92%A9=value';
app.run([]);
assertTrue(parameterWasFound);
@@ -103,12 +95,10 @@ function testParameterNameIsUriDecoded() {
function testParameterValueIsUriDecoded() {
let expectedValue = 'válué💩';
let actualValue;
- app._webActions = {
- getUriDecodedValueAndQuit: function(dict) {
- actualValue = dict['param'];
- app.quit();
- }
- };
+ app.define_web_action('getUriDecodedValueAndQuit', function(dict) {
+ actualValue = dict['param'];
+ app.quit();
+ });
app.webActionToTest = 'endless://getUriDecodedValueAndQuit?param=v%C3%A1lu%C3%A9%F0%9F%92%A9';
app.run([]);
assertEquals(expectedValue, actualValue);
@@ -124,17 +114,45 @@ function testParameterValueIsUriDecoded() {
function testWebActionIsCalledWithBlankParameter() {
let parameterWasFound = false;
let parameterValue;
- app._webActions = {
- getBlankValueAndQuit: function(dict) {
- parameterWasFound = ('param' in dict);
- if(parameterWasFound)
- parameterValue = dict['param'];
- app.quit();
- }
- };
+ app.define_web_action('getBlankValueAndQuit', function(dict) {
+ parameterWasFound = ('param' in dict);
+ if(parameterWasFound)
+ parameterValue = dict['param'];
+ app.quit();
+ });
app.webActionToTest = 'endless://getBlankValueAndQuit?param=';
app.run([]);
assertTrue(parameterWasFound);
assertNotUndefined(parameterValue);
assertEquals('', parameterValue);
}
+
+function testWebActionIsUriDecoded() {
+ let actionWasCalled = false;
+ app.define_web_action('äction💩Quit', function(dict) {
+ actionWasCalled = true;
+ app.quit();
+ });
+ app.webActionToTest = 'endless://%C3%A4ction%F0%9F%92%A9Quit';
+ app.run([]);
+ assertTrue(actionWasCalled);
+}
+
+function testDefineMultipleActionsOverride() {
+ let actionWasCalled = false;
+ app.define_web_actions({
+ quitApplication: function() {
+ actionWasCalled = true;
+ app.quit();
+ }
+ });
+ app.webActionToTest = 'endless://quitApplication';
+ app.run([]);
+ assertTrue(actionWasCalled);
+}
+
+function testDefineBadAction() {
+ assertRaises(function() {
+ app.define_web_action('badAction', 'not a function');
+ });
+}
diff --git a/webhelper/webhelper.js b/webhelper/webhelper.js
index 1992973..32ad4ec 100644
--- a/webhelper/webhelper.js
+++ b/webhelper/webhelper.js
@@ -1,8 +1,12 @@
const Endless = imports.gi.Endless;
+const Format = imports.format;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
const WebKit = imports.gi.WebKit;
+String.prototype.format = Format.format;
+
const EOS_URI_SCHEME = 'endless://';
/**
@@ -17,8 +21,8 @@ const EOS_URI_SCHEME = 'endless://';
*
* WebHelper solves that problem by detecting when the web page navigates to a
* custom action URI.
- * The custom URI corresponds to a function that you define in
- * <Application._webActions>, and you can pass parameters to the
+ * The custom URI corresponds to a function that you define using
+ * <Application.define_web_action()>, and you can pass parameters to the
* function.
*
* Another often-encountered problem is localizing text through the same API as
@@ -42,18 +46,70 @@ const Application = new Lang.Class({
Name: 'WebApplication',
Extends: Endless.Application,
+ _init: function(props) {
+ this._webActions = {};
+ this._translationFunction = null;
+ this.parent(props);
+ },
+
+ /**
+ * Method: define_web_action
+ * Define an action that may be invoked from a WebView
+ *
+ * Parameters:
+ * name - a string, which must be a valid URI location.
+ * implementation - a function (see Callback Parameters below.)
+ *
+ * Callback Parameters:
+ * dict - object containing properties corresponding to the query
+ * parameters that the web action was called with
+ *
+ * Sets up an action that may be invoked from an HTML document inside a
+ * WebView, or from the in-browser Javascript environment inside a WebView.
+ * If you set up an action "setVolume" as follows:
+ * > app.define_web_action('setVolume', function(dict) { ... });
+ * Then you can invoke the action inside the HTML document, e.g. as the
+ * target of a link, as follows:
+ * > <a href="endless://setVolume?volume=11">This one goes to 11</a>
+ * Or from the in-browser Javascript, by navigating to the action URI, as
+ * follows:
+ * > window.location.href = 'endless://setVolume?volume=11';
+ *
+ * In both cases, the function would then be called with the _dict_
+ * parameter equal to
+ * > { "volume": "11" }
+ *
+ * If an action called _name_ is already defined, the new _implementation_
+ * replaces the old one.
+ */
+ define_web_action: function(name, implementation) {
+ if(typeof implementation != 'function') {
+ throw new Error('The implementation of a web action must be a ' +
+ 'function.');
+ }
+ this._webActions[name] = implementation;
+ },
+
/**
- * Property: _webActions
- * Set of actions that may be invoked from a WebView
+ * Method: define_web_actions
+ * Define several web actions at once
*
- * Declare them as a function that takes a dict as a parameter, and use
- * links with the format "endless://actionName?parameter=value"
+ * Parameters:
+ * dict - an object, with web action names as property names, and their
+ * implementations as values
+ *
+ * Convenience method to define more than one web action at once.
+ * Calls <define_web_action()> on each property of _dict_.
*
- * *Note* This API will likely disappear and be replaced by a method
- * add_web_action().
- * That is the reason for the underscore starting the name.
+ * *Note* This API is Javascript-only. It will not be implemented in C.
*/
- _webActions: { },
+ define_web_actions: function(dict) {
+ for(let key in dict) {
+ if(dict.hasOwnProperty(key)) {
+ this.define_web_action(key, dict[key]);
+ }
+ }
+ },
/**
* Callback: web_actions_handler
@@ -80,7 +136,7 @@ const Application = new Lang.Class({
// get the name and parameters for the desired function
let f_call = uri.substring(EOS_URI_SCHEME.length, uri.length).split('?');
- var function_name = f_call[0];
+ var function_name = decodeURI(f_call[0]);
var parameters = {};
if(f_call[1]) {
@@ -101,14 +157,15 @@ const Application = new Lang.Class({
if(this._webActions[function_name])
Lang.bind(this, this._webActions[function_name])(parameters);
else
- throw new Error("Undefined WebHelper action '%s'. Did you add it " +
- "to your app's _webActions object?".format(function_name));
+ throw new Error("Undefined WebHelper action '%s'. Did you define " +
+ "it with WebHelper.Application.define_web_action()?".format(
+ function_name));
policy_decision.ignore();
return true;
},
-// convenience functions
+ // convenience function
_getElementById: function(webview, id) {
// WebKit.DOMDocument
@@ -119,18 +176,43 @@ const Application = new Lang.Class({
},
/**
- * Property: _translationFunction
- * Function which transforms all translatable text
+ * Method: set_translation_function
+ * Define function which transforms all translatable text
+ *
+ * Parameters:
+ * translation_function - a function, or null
*
* When you plan to use the <translate_html()> function to translate text in
* your web application, set this property to the translation function.
* The function must take one parameter, a string, and also return a
- * string -- for example, gettext().
+ * string.
+ * The canonical example is gettext().
+ *
+ * Pass null for _translation_function_ to unset the translation function.
*
- * *Note* This API will likely disappear and be replaced by a read-write
- * translation_function property.
- * That is the reason for the underscore starting the name.
+ * If this function has not been called, or has most recently been called
+ * with null, then it is illegal to call <translate_html()>.
*/
+ set_translation_function: function(translation_function) {
+ if(translation_function !== null
+ && typeof translation_function !== 'function') {
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ }
+ this._translationFunction = translation_function;
+ },
+
+ /**
+ * Method: get_translation_function
+ * Retrieve the currently set translation function
+ *
+ * Returns:
+ * the translation function previously set with
+ * <set_translation_function()>, or null if none is currently set.
+ */
+ get_translation_function: function() {
+ return this._translationFunction;
+ },
/**
* Method: translate_html
@@ -152,12 +234,12 @@ const Application = new Lang.Class({
*
* Then after the web view has finished loading, call <translate_html()> on
* it, and each of the marked strings will be passed to the function that
- * you specify using the <_translationFunction> property.
- * The return value from <_translationFunction> will be substituted into the
- * HTML instead of the original string.
+ * you specify using <set_translation_function()> property.
+ * The return value from the translation function will be substituted into
+ * the HTML instead of the original string.
*
* Example:
- * > app._translationFunction = _;
+ * > app.set_translation_function(_);
* > webview.connect('notify::load-status',
* > Lang.bind(app, function(webview) {
* > if(webview.load_status == WebKit.LoadStatus.FINISHED)
@@ -177,7 +259,8 @@ const Application = new Lang.Class({
// Translate the text
if(typeof this._translationFunction !== 'function')
throw new Error("No suitable translation function was found. " +
- "Did you forget to set '_translationFunction' on your app?");
+ "Did you forget to call 'set_translation_function()' on " +
+ "your app?");
element.inner_html = this._translationFunction(element.inner_text);
}
}