diff options
author | mattdangerw <mattdangerw@gmail.com> | 2013-09-18 15:13:09 -0700 |
---|---|---|
committer | mattdangerw <mattdangerw@gmail.com> | 2013-09-18 15:13:09 -0700 |
commit | e965fb73d552007690dab882bdc1a5e7a720c81a (patch) | |
tree | f9233a5502dccc2872184f60372b02cc15b32c5d | |
parent | e72607e0eac665f46676124f1038afe653b487ae (diff) | |
parent | dcab98606ce9aa37d7f980e2365e08af5458a5f3 (diff) |
Merge pull request #298 from endlessm/issues/290
Unit tests for WebHelper
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile.am | 9 | ||||
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | test/Makefile.am | 20 | ||||
-rw-r--r-- | test/smoke-tests/webview/first_page.html | 54 | ||||
-rw-r--r-- | test/tools/eos-run-test/sanitycheck.js | 3 | ||||
-rw-r--r-- | test/webhelper/smoke-tests/webview.js (renamed from test/smoke-tests/webview.js) | 63 | ||||
-rw-r--r-- | test/webhelper/testTranslate.js | 72 | ||||
-rw-r--r-- | test/webhelper/testWebActions.js | 140 | ||||
-rw-r--r-- | tools/Makefile.am.inc | 3 | ||||
-rw-r--r-- | tools/eos-run-test.in | 150 | ||||
-rw-r--r-- | webhelper/webhelper.js | 78 |
12 files changed, 489 insertions, 105 deletions
@@ -8,6 +8,7 @@ endless/eosresource.c endless/eosresource-private.h data/eos-wikipedia-domain.gresource wikipedia/config.js +tools/eos-run-test *.py[cod] diff --git a/Makefile.am b/Makefile.am index feee832..b15a47d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -146,6 +146,10 @@ gjsoverridedir = ${datadir}/gjs-1.0/overrides dist_gjsoverride_DATA = \ overrides/Endless.js +# # # SDK TOOLS # # # + +include $(top_srcdir)/tools/Makefile.am.inc + # # # INSTALLED M4 MACROS # # # m4dir = ${datadir}/aclocal @@ -156,8 +160,3 @@ m4_DATA = \ # # # TESTS # # # include $(top_srcdir)/test/Makefile.am - -# Run tests when running 'make check' -TESTS = test/run-tests -LOG_COMPILER = gtester -AM_LOG_FLAGS = -k --verbose diff --git a/configure.ac b/configure.ac index eb450a6..a74e3e0 100644 --- a/configure.ac +++ b/configure.ac @@ -210,6 +210,7 @@ AC_CONFIG_FILES([ docs/reference/endless/version.xml $EOS_SDK_API_NAME.pc ]) +AC_CONFIG_FILES([tools/eos-run-test], [chmod +x tools/eos-run-test]) AC_CONFIG_HEADERS([config.h]) dnl Header with system-dependent #defines # Do the output AC_OUTPUT diff --git a/test/Makefile.am b/test/Makefile.am index b4217c1..4124c52 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -24,3 +24,23 @@ test_run_tests_LDADD = $(TEST_LIBS) test_smoke_tests_hello_SOURCES = test/smoke-tests/hello.c test_smoke_tests_hello_CPPFLAGS = $(TEST_FLAGS) test_smoke_tests_hello_LDADD = $(TEST_LIBS) + +javascript_tests = \ + test/tools/eos-run-test/sanitycheck.js \ + test/webhelper/testTranslate.js \ + test/webhelper/testWebActions.js \ + $(NULL) +EXTRA_DIST += $(javascript_tests) + +# Run tests when running 'make check' +TESTS = \ + test/run-tests \ + $(javascript_tests) \ + $(NULL) +TEST_EXTENSIONS = .js +JS_LOG_COMPILER = tools/eos-run-test +AM_JS_LOG_FLAGS = \ + --include-path=$(top_srcdir)/webhelper \ + $(NULL) +LOG_COMPILER = gtester +AM_LOG_FLAGS = -k --verbose diff --git a/test/smoke-tests/webview/first_page.html b/test/smoke-tests/webview/first_page.html deleted file mode 100644 index 33a6a8f..0000000 --- a/test/smoke-tests/webview/first_page.html +++ /dev/null @@ -1,54 +0,0 @@ -<html> - -<head> -<title>First page</title> - -<style> -p, form { - width: 50%; - padding: 1em; - background: #FFFFFF; -} - -body { - background: #EEEEEE; -} -</style> -</head> - -<body> - -<h1>First page</h1> - -<p> -<a href="endless://moveToPage?name=page2">Move to page 2</a> -</p> - -<p> -<a href="endless://showMessageFromParameter?msg=This%20is%20a%20message%20from%20the%20URL%20parameter">Show message from parameter in this URL</a> -</p> - -<form action="endless://showMessageFromParameter"> -<input name="msg" value="I am in a form!"/> -<input type="submit" value="Show message using a form"/> -</form> - -<p> -<input id="inputformessage" value="my ID is inputformessage"/> -<a href="endless://showMessageFromInputField?id=inputformessage">Show message using the <input>'s ID</a> -</p> - -<p> -<a href="http://wikipedia.org">Regular link to a Web site</a> -</p> - -<p> -<a href="endless://addStars?id=starspan">I want stars!</a> <span id="starspan" /> -</p> - -<p> -This is text that will be translated: <span name="translatable">Hello, world!</span> -</p> - -</body> -</html>
\ No newline at end of file diff --git a/test/tools/eos-run-test/sanitycheck.js b/test/tools/eos-run-test/sanitycheck.js new file mode 100644 index 0000000..4bb3a80 --- /dev/null +++ b/test/tools/eos-run-test/sanitycheck.js @@ -0,0 +1,3 @@ +function testNothing() { + assertEquals(2, 2); +}
\ No newline at end of file diff --git a/test/smoke-tests/webview.js b/test/webhelper/smoke-tests/webview.js index 98226f3..fe2a655 100644 --- a/test/smoke-tests/webview.js +++ b/test/webhelper/smoke-tests/webview.js @@ -11,10 +11,61 @@ const WebHelper = imports.webhelper; const TEST_APPLICATION_ID = 'com.endlessm.example.test-webview'; +const TEST_HTML = '\ +<html> \ +<head> \ +<title>First page</title> \ +<style> \ +p, form { \ + width: 50%; \ + padding: 1em; \ + background: #FFFFFF; \ +} \ +body { \ + background: #EEEEEE; \ +} \ +</style> \ +</head> \ +\ +<body> \ +<h1>First page</h1> \ +\ +<p><a href="endless://moveToPage?name=page2">Move to page 2</a></p> \ +\ +<p><a \ +href="endless://showMessageFromParameter?msg=This%20is%20a%20message%20from%20the%20URL%20parameter">Show \ +message from parameter in this URL</a></p> \ +\ +<form action="endless://showMessageFromParameter"> \ +<input name="msg" value="I am in a form!"/> \ +<input type="submit" value="Show message using a form"/> \ +</form> \ +\ +<p> \ +<input id="inputformessage" value="my ID is inputformessage"/> \ +<a href="endless://showMessageFromInputField?id=inputformessage">Show message \ +using the <input>\'s ID</a> \ +</p> \ +\ +<p><a href="http://wikipedia.org">Regular link to a Web site</a></p> \ +\ +<p><a href="endless://addStars?id=starspan">I want \ +stars!</a> <span id="starspan"/></p> \ +\ +<p>This is text that will be italicized: <span name="translatable">Hello, \ +world!</span></p> \ +\ +</body> \ +</html>'; + const TestApplication = new Lang.Class({ Name: 'TestApplication', Extends: WebHelper.Application, + _translationFunction: function(string) { + return string.italics(); + }, + /* *** ACTIONS AVAILABLE FROM THE WEB VIEW *** */ _webActions: { @@ -64,21 +115,17 @@ const TestApplication = new Lang.Class({ this.parent(); this._webview = new WebKit.WebView(); - - let cwd = GLib.get_current_dir(); - let target = cwd + '/test/smoke-tests/webview/first_page.html'; - this._webview.load_uri(GLib.filename_to_uri(target, null)); - - this._webview.connect('notify::load-status', + 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._translateHTML (web_view, 'pt_BR'); + this.translate_html(web_view); } })); this._webview.connect('navigation-policy-decision-requested', - Lang.bind(this, this._onNavigationRequested)); + Lang.bind(this, this.web_actions_handler)); this._page1 = new Gtk.ScrolledWindow(); this._page1.add(this._webview); diff --git a/test/webhelper/testTranslate.js b/test/webhelper/testTranslate.js new file mode 100644 index 0000000..10e389c --- /dev/null +++ b/test/webhelper/testTranslate.js @@ -0,0 +1,72 @@ +const Endless = imports.gi.Endless; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const WebHelper = imports.webhelper; +const WebKit = imports.gi.WebKit; + +const TestClass = new Lang.Class({ + Name: 'testclass', + Extends: WebHelper.Application, + + vfunc_startup: function() { + this.parent(); + this.webview = new WebKit.WebView(); + let string = '<html><body><p name="translatable">Translate Me</p></body></html>'; + this.webview.load_string(string, 'text/html', 'UTF-8', 'file://'); + this.win = new Endless.Window({ + application: this + }); + this.scrolled = new Gtk.ScrolledWindow(); + this.scrolled.add(this.webview); + this.win.page_manager.add(this.scrolled); + + this.webview.connect('notify::load-status', Lang.bind(this, function() { + if(this.webview.load_status === WebKit.LoadStatus.FINISHED) { + this.translate_html(this.webview); + this.quit(); + } + })); + + // Add an upper bound on how long the app runs, in case app.quit() does + // not get called + GLib.timeout_add_seconds(GLib.PRIORITY_HIGH, 5, Lang.bind(this, function() { + this.quit(); + })); + } +}); + +let app; + +function setUp() { + // Generate a unique ID for each app instance that we test + let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time(); + app = new TestClass({ + application_id: id_string + }); +} + +function testStringIsTranslated() { + let translationFunctionWasCalled = false; + let translationFunctionCalledWithString; + app._translationFunction = 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 +// across FFI interfaces (e.g. in GObject callbacks.) + +// function testMissingTranslationFunctionIsHandled() { +// assertRaises(app.run([])); +// } + +// function testBadTranslationFunctionIsHandled() { +// app._translationFunction = "I am not a function"; +// assertRaises(app.run([])); +// }
\ No newline at end of file diff --git a/test/webhelper/testWebActions.js b/test/webhelper/testWebActions.js new file mode 100644 index 0000000..585ab00 --- /dev/null +++ b/test/webhelper/testWebActions.js @@ -0,0 +1,140 @@ +const Endless = imports.gi.Endless; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const WebHelper = imports.webhelper; +const WebKit = imports.gi.WebKit; + +const TestClass = new Lang.Class({ + Name: 'testclass', + Extends: WebHelper.Application, + + vfunc_startup: function() { + this.parent(); + this.webview = new WebKit.WebView(); + this.webview.connect('navigation-policy-decision-requested', + Lang.bind(this, this.web_actions_handler)); + let string = ('<html><head><meta http-equiv="refresh" content="0;url=' + + this.webActionToTest + '"></head><body></body></html>'); + this.webview.load_string(string, 'text/html', 'UTF-8', 'file://'); + this.win = new Endless.Window({ + application: this + }); + this.scrolled = new Gtk.ScrolledWindow(); + this.scrolled.add(this.webview); + this.win.page_manager.add(this.scrolled); + + // Add an upper bound on how long the app runs, in case app.quit() does + // not get called + GLib.timeout_add_seconds(GLib.PRIORITY_HIGH, 5, Lang.bind(this, function() { + this.quit(); + })); + } +}); + +let app; + +function setUp() { + // Generate a unique ID for each app instance that we test + let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time(); + app = new TestClass({ + application_id: id_string + }); +} + +function testWebActionIsCalled() { + let actionWasCalled = false; + app._webActions = { + quitApplication: function() { + actionWasCalled = true; + app.quit(); + } + }; + app.webActionToTest = 'endless://quitApplication'; + app.run([]); + assertTrue(actionWasCalled); +} + +function testWebActionIsCalledWithParameter() { + let actionParameter; + app._webActions = { + getParameterAndQuit: function(dict) { + actionParameter = dict['param']; + app.quit(); + } + }; + app.webActionToTest = 'endless://getParameterAndQuit?param=value'; + app.run([]); + assertEquals('value', actionParameter); +} + +function testWebActionIsCalledWithManyParameters() { + let firstParameter, secondParameter, thirdParameter; + app._webActions = { + 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); + assertEquals('thesecond', secondParameter); + assertEquals('thethird', thirdParameter); +} + +function testParameterNameIsUriDecoded() { + let expectedParameter = 'päräm💩'; + let parameterWasFound = false; + app._webActions = { + 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); +} + +function testParameterValueIsUriDecoded() { + let expectedValue = 'válué💩'; + let actualValue; + app._webActions = { + 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); +} + +// This is commented out because GJS cannot catch exceptions across FFI +// interfaces (e.g. in GObject callbacks.) +// function testBadActionIsNotCalled() { +// app.webActionToTest = 'endless://nonexistentAction?param=value'; +// assertRaises(function() { app.run([]); }); +// } + +function testWebActionIsCalledWithBlankParameter() { + let parameterWasFound = false; + let parameterValue; + app._webActions = { + 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); +} diff --git a/tools/Makefile.am.inc b/tools/Makefile.am.inc new file mode 100644 index 0000000..1a790ce --- /dev/null +++ b/tools/Makefile.am.inc @@ -0,0 +1,3 @@ +# Copyright 2013 Endless Mobile, Inc. + +bin_SCRIPTS = tools/eos-run-test diff --git a/tools/eos-run-test.in b/tools/eos-run-test.in new file mode 100644 index 0000000..888e53b --- /dev/null +++ b/tools/eos-run-test.in @@ -0,0 +1,150 @@ +#!/usr/bin/gjs +const Format = imports.format; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const JsUnit = imports.jsUnit; +const Lang = imports.lang; +const System = imports.system; + +String.prototype.format = Format.format; +// Monkeypatch System.programInvocationName if not in this version of GJS +if(!('programInvocationName' in System)) + System.programInvocationName = 'eos-run-test'; + +const PACKAGE_VERSION = '@PACKAGE_VERSION@'; +const JS_EXTENSION = '.js'; + +/** + * usage: + * + * Print command-line help message. + */ +function usage() { + print('Run a jsUnit unit test.\n'); + print('Usage: %s [options] TEST_FILES\n'.format( + System.programInvocationName)); + print('Options:'); + print(' --help This help message'); + print(' --version Print version and exit'); + throw(0); // FIXME System.exit(0); +} + +/** + * version: + * + * Print command-line version output. + */ +function version() { + print('%s %s - Discover unit tests in a source tree'.format( + System.programInvocationName, PACKAGE_VERSION)); + throw(0); // FIXME System.exit(0); +} + +if(ARGV.indexOf('--help') != -1) + usage(); +if(ARGV.indexOf('--version') != -1) + version(); +if(ARGV.length < 1) + usage(); + +// Import JsUnit into global namespace +if(!('assertEquals' in this)) + Lang.copyPublicProperties(JsUnit, this); + +function printError(error) { + const SYNTAX_ERROR = '[object Error]'; + print(" " + error.message); + + if(error.stackTrace) { + let stackTrace = error.stackTrace.split('\n'); + stackTrace.forEach(function(line) { + if(line.length > 0){ + let prefix = ' --'; + if (error.type == SYNTAX_ERROR) + prefix += '> '; + + print(prefix + line); + } + }); + } +} + +function executeTest(testModule, test) { + let result = { name: test }; + print("Running:", test); + if(testModule.setUp) { + testModule.setUp(); + } + let startTime = GLib.get_real_time(); + try { + testModule[test](); + } catch(e) { + print(" ERROR! >>>>>", test, "<<<<<"); + + result.error = { + type: Object.toString(e), + message: e.message || e.jsUnitMessage, + stackTrace: e.stack || e.stackTrace + }; + printError(result.error); + print('\n'); + } finally { + if(testModule.tearDown) { + testModule.tearDown(); + } + } + + result.time = GLib.get_real_time() - startTime; + return result; +} + +function executeTestsForFile(file) { + let testFile = file.get_basename(); + let testModuleName = testFile.slice(0, testFile.indexOf('.js')); + print('File:', testFile); + let oldSearchPath = imports.searchPath; + imports.searchPath.unshift(file.get_parent().get_path()); + let testModule = imports[testModuleName]; + imports.searchPath = oldSearchPath; + + let results = []; + Object.keys(testModule).forEach(function(key) { + if(key.indexOf('test') === 0) { + results.push(executeTest(testModule, key)); + } + }); + + return results; +} + +function getTotalsFromResults(results) { + let testsRunCount = 0; + let testsFailedCount = 0; + let testsPassedCount = 0; + + results.forEach(function(result) { + if ('error' in result) { + testsFailedCount++; + } + testsRunCount++; + }); + + testsPassedCount = testsRunCount - testsFailedCount; + return { + testsRunCount: testsRunCount, + testsFailedCount: testsFailedCount, + testsPassedCount: testsPassedCount + }; +} + +let fileToTest = Gio.File.new_for_path(ARGV[0]); +let results = executeTestsForFile(fileToTest); +let totals = getTotalsFromResults(results); +let totalsString = "Ran %d tests (%d Passed, %d Failed)".format( + totals.testsRunCount, totals.testsPassedCount, totals.testsFailedCount); +print(totalsString); + +if (totals.testsFailedCount > 0){ + printerr("Test(s) did not complete successfully"); + throw(1); // FIXME System.exit(1); +} diff --git a/webhelper/webhelper.js b/webhelper/webhelper.js index 8b78931..ccce9e7 100644 --- a/webhelper/webhelper.js +++ b/webhelper/webhelper.js @@ -16,47 +16,46 @@ const Application = new Lang.Class({ _webActions: { }, -// This callback does the translation from URI to action -// this._webview.connect('navigation-policy-decision-requested', -// Lang.bind(this, this._webHelper.onNavigationRequested)); + // This callback does the translation from URI to action + // webview.connect('navigation-policy-decision-requested', + // Lang.bind(this, this.web_actions_handler)); - _onNavigationRequested : function(web_view, frame, request, - navigation_action, policy_decision, - user_data) { + web_actions_handler: function(webview, frame, request, action, policy_decision) { let uri = request.get_uri(); - if(uri.indexOf(EOS_URI_SCHEME) == 0) { - // 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 parameters = {}; - - if(f_call[1]) { - // there are parameters - let params = f_call[1].split('&'); - params.forEach(function(entry) { - let param = entry.split('='); - - if(param.length == 2) { - param[0] = decodeURIComponent(param[0]); - param[1] = decodeURIComponent(param[1]); - // and now we add it... - parameters[param[0]] = param[1]; - } - }); - } - - if(this._webActions[function_name]) - Lang.bind(this, this._webActions[function_name])(parameters); - else - print('Unknown function '+function_name); - - policy_decision.ignore(); - return true; - } else { + if(uri.indexOf(EOS_URI_SCHEME) !== 0) { // this is a regular URL, just navigate there return false; } + + // 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 parameters = {}; + + if(f_call[1]) { + // there are parameters + let params = f_call[1].split('&'); + params.forEach(function(entry) { + let param = entry.split('='); + + if(param.length == 2) { + param[0] = decodeURIComponent(param[0]); + param[1] = decodeURIComponent(param[1]); + // and now we add it... + parameters[param[0]] = param[1]; + } + }); + } + + 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)); + + policy_decision.ignore(); + return true; }, // convenience functions @@ -69,7 +68,7 @@ const Application = new Lang.Class({ return dom.get_element_by_id(id); }, - _translateHTML: function(webview, lang) { + translate_html: function(webview) { let dom = webview.get_dom_document(); // WebKit.DOMNodeList @@ -79,8 +78,11 @@ const Application = new Lang.Class({ // WebKit.DOMNode let element = translatable.item(i); - // TODO here is where we would do the translation - element.inner_html = '<i>' + element.inner_text + '</i>'; + // 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?"); + element.inner_html = this._translationFunction(element.inner_text); } } }); |