From 496433adababa1d63756c8cbe558f062ae2c3754 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 9 Feb 2018 14:52:05 +0700 Subject: [PATCH 01/26] Initial engine unit test infrastructure in place with keyboard load test. --- web/unit_tests/CI.conf.js | 16 ++++++-- web/unit_tests/cases/engine.js | 40 +++++++++++++++++++ .../json/keyboards/lao_2008_basic.json | 10 +++++ web/unit_tests/manual.conf.js | 9 ++++- .../resources/keyboards/lao_2008_basic.js | 1 + web/unit_tests/test_utils.js | 12 ++++-- 6 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 web/unit_tests/cases/engine.js create mode 100644 web/unit_tests/json/keyboards/lao_2008_basic.json create mode 100644 web/unit_tests/resources/keyboards/lao_2008_basic.js diff --git a/web/unit_tests/CI.conf.js b/web/unit_tests/CI.conf.js index 19e2107d5d8..8333503da56 100644 --- a/web/unit_tests/CI.conf.js +++ b/web/unit_tests/CI.conf.js @@ -121,10 +121,10 @@ module.exports = function(config) { /* * Final selection of the sets to be used for BrowserStack testing. */ - var FINAL_LAUNCHER_DEFS = mergeLaunchers( CURRENT_MAC_LAUNCHERS, + var FINAL_LAUNCHER_DEFS = mergeLaunchers( CURRENT_ANDROID_LAUNCHERS, CURRENT_IOS_LAUNCHERS, CURRENT_WIN_LAUNCHERS, - CURRENT_ANDROID_LAUNCHERS); + CURRENT_MAC_LAUNCHERS); var FINAL_BROWSER_LIST = toBrowserList(FINAL_LAUNCHER_DEFS); @@ -146,6 +146,9 @@ module.exports = function(config) { captureTimeout: 180000, // in milliseconds + // Avoids generating a 'fail' exit code if one of our selected browsers on BrowserStack goes poof. + failOnEmptyTestSuite: false, + // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha', 'chai', 'fixture'], @@ -156,6 +159,7 @@ module.exports = function(config) { 'unit_tests/test_utils.js', // A basic utility script useful for constructing tests 'unit_tests/modernizr.js', // A dependency-managed utility script that helps with browser feature detection. 'unit_tests/cases/**/*.js', // Where the tests actually reside. + 'unit_tests/json/**/*.json', // Where pre-loaded JSON resides. {pattern: 'unit_tests/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. {pattern: 'release/unminified/web/**/*.css', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/**/*.gif', watched: false, served: true, included: false}, // OSK resources @@ -176,10 +180,16 @@ module.exports = function(config) { ], + jsonFixturesPreprocessor: { + stripPrefix: 'unit_tests/json', + variableName: '__json__' + }, + // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - 'unit_tests/fixtures/**/*.html' : ['html2js'] + 'unit_tests/fixtures/**/*.html' : ['html2js'], + 'unit_tests/json/**/*.json' : ['json_fixtures'] }, diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js new file mode 100644 index 00000000000..29d902d4681 --- /dev/null +++ b/web/unit_tests/cases/engine.js @@ -0,0 +1,40 @@ +var assert = chai.assert; + +describe('Engine', function() { + + before(function(done) { + this.timeout(10000); + + fixture.setBase('unit_tests/fixtures'); + setupKMW(); + + // Pass the initTimer method ousr 'done' callback so it can handle our initialization delays for us. + initTimer(done); + }); + + beforeEach(function() { + fixture.load("singleInput.html"); + }); + + after(function() { + teardownKMW(); + }); + + afterEach(function() { + fixture.cleanup(); + }); + + describe('Keyboards', function() { + it('Successfully loads a locally-stored keyboard.', function(done) { + var laoStub = fixture.load("/keyboards/lao_2008_basic.json", true); + + keyman.addKeyboards(laoStub); + keyman.setActiveKeyboard("Keyboard_lao_2008_basic", "lao"); + + window.setTimeout(function() { + assert.isTrue(keyman.getActiveKeyboard() == "Keyboard_lao_2008_basic"); + done(); + }, 1000); + }); + }); +}); \ No newline at end of file diff --git a/web/unit_tests/json/keyboards/lao_2008_basic.json b/web/unit_tests/json/keyboards/lao_2008_basic.json new file mode 100644 index 00000000000..913df32a45f --- /dev/null +++ b/web/unit_tests/json/keyboards/lao_2008_basic.json @@ -0,0 +1,10 @@ +{ + "id": "lao_2008_basic", + "name":"Lao Basic", + "languages":{ + "id":"lao", + "name": "Lao", + "region": "Asia" + }, + "filename":"resources/keyboards/lao_2008_basic.js" +} \ No newline at end of file diff --git a/web/unit_tests/manual.conf.js b/web/unit_tests/manual.conf.js index 5536a30aa64..77cefec01a1 100644 --- a/web/unit_tests/manual.conf.js +++ b/web/unit_tests/manual.conf.js @@ -20,6 +20,7 @@ module.exports = function(config) { 'unit_tests/test_utils.js', // A basic utility script useful for constructing tests 'unit_tests/modernizr.js', // A dependency-managed utility script that helps with browser feature detection. 'unit_tests/cases/**/*.js', // Where the tests actually reside. + 'unit_tests/json/**/*.json', // Where pre-loaded JSON resides. {pattern: 'unit_tests/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. {pattern: 'release/unminified/web/**/*.css', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/**/*.gif', watched: false, served: true, included: false}, // OSK resources @@ -39,10 +40,16 @@ module.exports = function(config) { exclude: [ ], + jsonFixturesPreprocessor: { + stripPrefix: 'unit_tests/json', + variableName: '__json__' + }, + // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - 'unit_tests/fixtures/**/*.html' : ['html2js'] + 'unit_tests/fixtures/**/*.html' : ['html2js'], + 'unit_tests/json/**/*.json' : ['json_fixtures'] }, // test results reporter to use diff --git a/web/unit_tests/resources/keyboards/lao_2008_basic.js b/web/unit_tests/resources/keyboards/lao_2008_basic.js new file mode 100644 index 00000000000..fa1eefddb44 --- /dev/null +++ b/web/unit_tests/resources/keyboards/lao_2008_basic.js @@ -0,0 +1 @@ +KeymanWeb.KR(new Keyboard_lao_2008_basic()); function Keyboard_lao_2008_basic() {this.KI="Keyboard_lao_2008_basic";this.KN="Lao 2008 Basic";this.KV={F:' 1em "Saysettha OT"',K102:0,BK:new Array("* ","ຢ ","ຟ ","ໂ ","ຖ ","ກຸ ","ກູ ","ຄ ","ຕ ","ຈ ","ຂ ","ຊ ","ກໍ ","","","","ກົ ","ໄ ","ກໍາ","ພ ","ະ ","ກິ ","ກີ ","ຮ ","ນ ","ຍ ","ບ ","ລ ","\\ ","","","","ກັ ","ຫ ","ກ ","ດ ","ເ ","ກ້ ","ກ່ ","າ ","ສ ","ວ ","ງ ","","","","","","","ຜ ","ປ ","ແ ","ອ ","ກຶ ","ກື ","ທ ","ມ ","ໃ ","ຝ ","","","","","","","/ ","1 ","2 ","3 ","4 ","ກ໌ ","ກຼ ","5 ","6 ","7 ","8 ","9 ","ກໍ່ ","","","","ກົ້ ","0 ","ກໍ້າ ","_ ","+ ","ກິ‍້ ","ກີ້ ","ຣ ","ໜ ","ຽ ","- ","ຫຼ ",">:<","","","","ກັ້ ","; ",". ",", ",": "," "," ","! ","? ","% ","= ","","","","","","","\" ","( ","ຯ ","x ","ກຶ້ ","ກື້ ","ໆ ","ໝ ","$ ",") ","","","","",""," ","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""," ","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","₭","໑","໒","໓","໔","","","໕","໖","໗","໘","໙","€","","","","","໐","","","","","","","","","[","]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","")};this.KH='';this.KM=0;this.s12="ກຂຄງຈສຊຍດຕຖທນບປຜຝພຟມຢຣລວຫອຮໝໜຼ";this.s13="d07'9l-pf84mo[xz/r2,1I];svi{~";this.s33="QEYUABN+";this.gs=function(t,e){return this.g0(t,e);};this.g0=function(t,e){var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16480,48)){r=m=1;k.KO(0,t,"໘");}else if(k.KKM(e,16480,49)){r=m=1;k.KO(0,t,"໑");}else if(k.KKM(e,16480,50)){r=m=1;k.KO(0,t,"໒");}else if(k.KKM(e,16480,51)){r=m=1;k.KO(0,t,"໓");}else if(k.KKM(e,16480,52)){r=m=1;k.KO(0,t,"໔");}else if(k.KKM(e,16480,55)){r=m=1;k.KO(0,t,"໕");}else if(k.KKM(e,16480,56)){r=m=1;k.KO(0,t,"໖");}else if(k.KKM(e,16480,57)){r=m=1;k.KO(0,t,"໗");}else if(k.KKM(e,16480,87)){r=m=1;k.KO(0,t,"໐");}else if(k.KKM(e,16384,96)){r=m=1;k.KO(0,t,"0");}else if(k.KKM(e,16384,97)){r=m=1;k.KO(0,t,"1");}else if(k.KKM(e,16384,98)){r=m=1;k.KO(0,t,"2");}else if(k.KKM(e,16384,99)){r=m=1;k.KO(0,t,"3");}else if(k.KKM(e,16384,100)){r=m=1;k.KO(0,t,"4");}else if(k.KKM(e,16384,101)){r=m=1;k.KO(0,t,"5");}else if(k.KKM(e,16384,102)){r=m=1;k.KO(0,t,"6");}else if(k.KKM(e,16384,103)){r=m=1;k.KO(0,t,"7");}else if(k.KKM(e,16384,104)){r=m=1;k.KO(0,t,"8");}else if(k.KKM(e,16384,105)){r=m=1;k.KO(0,t,"9");}else if(k.KKM(e,16384,106)){r=m=1;k.KO(0,t,"*");}else if(k.KKM(e,16384,107)){r=m=1;k.KO(0,t,"+");}else if(k.KKM(e,16384,109)){r=m=1;k.KO(0,t,"-");}else if(k.KKM(e,16384,110)){r=m=1;k.KO(0,t,".");}else if(k.KKM(e,16384,111)){r=m=1;k.KO(0,t,"/");}else if(k.KKM(e,16480,187)){r=m=1;k.KO(0,t,"€");}else if(k.KKM(e,16480,189)){r=m=1;k.KO(0,t,"໙");}else if(k.KKM(e,16480,192)){r=m=1;k.KO(0,t,"₭");}else if(k.KKM(e,16480,219)){r=m=1;k.KO(0,t,"[");}else if(k.KKM(e,16480,221)){r=m=1;k.KO(0,t,"]");}if(!m&&k.KIK(e)){r=this.g1(t,e);}return r;};this.g1=function(t,e){var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16400,49)){r=m=1;k.KO(0,t,"1");}else if(k.KKM(e,16400,222)){r=m=1;k.KO(0,t,"=");}else if(k.KKM(e,16400,51)){r=m=1;k.KO(0,t,"3");}else if(k.KKM(e,16400,52)){r=m=1;k.KO(0,t,"4");}else if(k.KKM(e,16400,53)&&k.KCM(1,t,"ຳ",1)){r=m=1;k.KO(1,t,"໌");}else if(k.KKM(e,16400,53)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,53)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,53)&&k.KA(0,k.KC(1,1,t),this.s20)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,53)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,53)){r=m=1;k.KO(0,t,"໌");}else if(k.KKM(e,16400,55)){r=m=1;k.KO(0,t,"5");}else if(k.KKM(e,16384,222)){r=m=1;k.KO(0,t,"ງ");}else if(k.KKM(e,16400,57)){r=m=1;k.KO(0,t,"7");}else if(k.KKM(e,16400,48)){r=m=1;k.KO(0,t,"8");}else if(k.KKM(e,16400,56)){r=m=1;k.KO(0,t,"6");}else if(k.KKM(e,16400,187)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,187)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,187)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,187)){r=m=1;k.KO(0,t,"ໍ່");}else if(k.KKM(e,16384,188)){r=m=1;k.KO(0,t,"ມ");}else if(k.KKM(e,16384,189)){r=m=1;k.KO(0,t,"ຊ");}else if(k.KKM(e,16384,190)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,190)){r=m=1;k.KO(0,t,"ໃ");}else if(k.KKM(e,16384,191)){r=m=1;k.KO(0,t,"ຝ");}else if(k.KKM(e,16384,48)){r=m=1;k.KO(0,t,"ຂ");}else if(k.KKM(e,16384,49)){r=m=1;k.KO(0,t,"ຢ");}else if(k.KKM(e,16384,50)){r=m=1;k.KO(0,t,"ຟ");}else if(k.KKM(e,16384,51)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,51)){r=m=1;k.KO(0,t,"ໂ");}else if(k.KKM(e,16384,52)){r=m=1;k.KO(0,t,"ຖ");}else if(k.KKM(e,16384,53)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,53)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,53)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,53)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,53)){r=m=1;k.KO(0,t,"ຸ");}else if(k.KKM(e,16384,54)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,54)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,54)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,54)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,54)){r=m=1;k.KO(0,t,"ູ");}else if(k.KKM(e,16384,55)){r=m=1;k.KO(0,t,"ຄ");}else if(k.KKM(e,16384,56)){r=m=1;k.KO(0,t,"ຕ");}else if(k.KKM(e,16384,57)){r=m=1;k.KO(0,t,"ຈ");}else if(k.KKM(e,16400,186)){r=m=1;k.KO(0,t,"%");}else if(k.KKM(e,16384,186)){r=m=1;k.KO(0,t,"ວ");}else if(k.KKM(e,16400,188)){r=m=1;k.KO(0,t,"ໝ");}else if(k.KKM(e,16384,187)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,187)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,187)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,187)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,187)){r=m=1;k.KO(0,t,"ໍ");}else if(k.KKM(e,16400,190)){r=m=1;k.KO(0,t,"$");}else if(k.KKM(e,16400,191)){r=m=1;k.KO(0,t,")");}else if(k.KKM(e,16400,50)){r=m=1;k.KO(0,t,"2");}else if(k.KKM(e,16400,65)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,65)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,65)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,65)){r=m=1;k.KO(0,t,"ັ້");}else if(k.KKM(e,16400,66)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,66)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,66)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,66)){r=m=1;k.KO(0,t,"ຶ້");}else if(k.KKM(e,16400,67)){r=m=1;k.KO(0,t,"ຯ");}else if(k.KKM(e,16400,68)){r=m=1;k.KO(0,t,".");}else if(k.KKM(e,16400,69)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,69)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,69)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,69)){r=m=1;k.KO(0,t,"້ຳ");}else if(k.KKM(e,16400,70)){r=m=1;k.KO(0,t,",");}else if(k.KKM(e,16400,71)){r=m=1;k.KO(0,t,":");}else if(k.KKM(e,16400,72)&&k.KCM(1,t,"ຳ",1)){r=m=1;k.KO(1,t,"໊");}else if(k.KKM(e,16400,72)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,72)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,72)&&k.KA(0,k.KC(1,1,t),this.s20)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,72)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,72)){r=m=1;k.KO(0,t,"໊");}else if(k.KKM(e,16400,73)){r=m=1;k.KO(0,t,"ຣ");}else if(k.KKM(e,16400,74)&&k.KCM(1,t,"ຳ",1)){r=m=1;k.KO(1,t,"໋");}else if(k.KKM(e,16400,74)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,74)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,74)&&k.KA(0,k.KC(1,1,t),this.s20)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,74)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,74)){r=m=1;k.KO(0,t,"໋");}else if(k.KKM(e,16400,75)){r=m=1;k.KO(0,t,"!");}else if(k.KKM(e,16400,76)){r=m=1;k.KO(0,t,"?");}else if(k.KKM(e,16400,77)){r=m=1;k.KO(0,t,"ໆ");}else if(k.KKM(e,16400,78)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,78)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,78)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,78)){r=m=1;k.KO(0,t,"ື້");}else if(k.KKM(e,16400,79)){r=m=1;k.KO(0,t,"ໜ");}else if(k.KKM(e,16400,80)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,80)){r=m=1;k.KO(0,t,"ຽ");}else if(k.KKM(e,16400,81)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,81)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,81)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,81)){r=m=1;k.KO(0,t,"ົ້");}else if(k.KKM(e,16400,82)){r=m=1;k.KO(0,t,"_");}else if(k.KKM(e,16400,83)){r=m=1;k.KO(0,t,";");}else if(k.KKM(e,16400,84)){r=m=1;k.KO(0,t,"+");}else if(k.KKM(e,16400,85)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,85)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,85)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,85)){r=m=1;k.KO(0,t,"ີ້");}else if(k.KKM(e,16400,86)){r=m=1;k.KO(0,t,"x");}else if(k.KKM(e,16400,87)){r=m=1;k.KO(0,t,"0");}else if(k.KKM(e,16400,88)){r=m=1;k.KO(0,t,"(");}else if(k.KKM(e,16400,89)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,89)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,89)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16400,89)){r=m=1;k.KO(0,t,"ິ້");}else if(k.KKM(e,16400,90)){r=m=1;k.KO(0,t,"\"");}else if(k.KKM(e,16384,219)){r=m=1;k.KO(0,t,"ບ");}else if(k.KKM(e,16384,221)){r=m=1;k.KO(0,t,"ລ");}else if(k.KKM(e,16400,54)){r=m=1;k.KO(0,t,"ຼ");}else if(k.KKM(e,16400,189)){r=m=1;k.KO(0,t,"9");}else if(k.KKM(e,16384,192)){r=m=1;k.KO(0,t,"*");}else if(k.KKM(e,16384,65)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,65)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,65)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,65)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,65)){r=m=1;k.KO(0,t,"ັ");}else if(k.KKM(e,16384,66)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,66)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,66)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,66)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,66)){r=m=1;k.KO(0,t,"ຶ");}else if(k.KKM(e,16384,67)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,67)){r=m=1;k.KO(0,t,"ແ");}else if(k.KKM(e,16384,68)){r=m=1;k.KO(0,t,"ກ");}else if(k.KKM(e,16384,69)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KO(-1,t,"ຳ");}else if(k.KKM(e,16384,69)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,69)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,69)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,69)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,69)){r=m=1;k.KO(0,t,"ຳ");}else if(k.KKM(e,16384,70)){r=m=1;k.KO(0,t,"ດ");}else if(k.KKM(e,16384,71)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,71)){r=m=1;k.KO(0,t,"ເ");}else if(k.KKM(e,16384,72)&&k.KCM(1,t,"ຳ",1)){r=m=1;k.KO(1,t,"້");}else if(k.KKM(e,16384,72)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,72)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,72)&&k.KA(0,k.KC(1,1,t),this.s20)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,72)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,72)){r=m=1;k.KO(0,t,"້");}else if(k.KKM(e,16384,73)){r=m=1;k.KO(0,t,"ຮ");}else if(k.KKM(e,16384,74)&&k.KCM(1,t,"ຳ",1)){r=m=1;k.KO(1,t,"່");}else if(k.KKM(e,16384,74)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,74)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,74)&&k.KA(0,k.KC(1,1,t),this.s20)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,74)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,74)){r=m=1;k.KO(0,t,"່");}else if(k.KKM(e,16384,75)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,75)&&k.KCM(1,t,"ະ",1)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,75)&&k.KCM(1,t,"າ",1)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,75)){r=m=1;k.KO(0,t,"າ");}else if(k.KKM(e,16384,76)){r=m=1;k.KO(0,t,"ສ");}else if(k.KKM(e,16384,77)){r=m=1;k.KO(0,t,"ທ");}else if(k.KKM(e,16384,78)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,78)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,78)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,78)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,78)){r=m=1;k.KO(0,t,"ື");}else if(k.KKM(e,16384,79)){r=m=1;k.KO(0,t,"ນ");}else if(k.KKM(e,16384,80)){r=m=1;k.KO(0,t,"ຍ");}else if(k.KKM(e,16384,81)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,81)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,81)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,81)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,81)){r=m=1;k.KO(0,t,"ົ");}else if(k.KKM(e,16384,82)){r=m=1;k.KO(0,t,"ພ");}else if(k.KKM(e,16384,83)){r=m=1;k.KO(0,t,"ຫ");}else if(k.KKM(e,16384,84)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,84)&&k.KCM(1,t,"ະ",1)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,84)){r=m=1;k.KO(0,t,"ະ");}else if(k.KKM(e,16384,85)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,85)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,85)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,85)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,85)){r=m=1;k.KO(0,t,"ີ");}else if(k.KKM(e,16384,86)){r=m=1;k.KO(0,t,"ອ");}else if(k.KKM(e,16384,87)&&k.KA(0,k.KC(1,1,t),this.s18)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,87)){r=m=1;k.KO(0,t,"ໄ");}else if(k.KKM(e,16384,88)){r=m=1;k.KO(0,t,"ປ");}else if(k.KKM(e,16384,89)&&k.KA(0,k.KC(1,1,t),this.s22)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,89)&&k.KA(0,k.KC(1,1,t),this.s14)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,89)&&k.KA(0,k.KC(1,1,t),this.s16)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,89)&&k.KA(0,k.KC(1,1,t),this.s24)){r=m=1;k.KB(t);}else if(k.KKM(e,16384,89)){r=m=1;k.KO(0,t,"ິ");}else if(k.KKM(e,16384,90)){r=m=1;k.KO(0,t,"ຜ");}else if(k.KKM(e,16400,219)){r=m=1;k.KO(0,t,"-");}else if(k.KKM(e,16400,220)){r=m=1;k.KO(0,t,"​");}else if(k.KKM(e,16400,221)){r=m=1;k.KO(0,t,"ຫຼ");}else if(k.KKM(e,16400,192)){r=m=1;k.KO(0,t,"/");}return r;};} \ No newline at end of file diff --git a/web/unit_tests/test_utils.js b/web/unit_tests/test_utils.js index b86cf349131..c061c714901 100644 --- a/web/unit_tests/test_utils.js +++ b/web/unit_tests/test_utils.js @@ -7,7 +7,7 @@ var setupKMW = function(kmwOptions) { var kmwOptions = { attachType:'auto', root:'source', - resources:'source' + resources:'../../../../source' }; if(ui) { @@ -21,8 +21,14 @@ var setupKMW = function(kmwOptions) { ui = kmwOptions.ui; kmwOptions.attachType = kmwOptions.attachType ? kmwOptions.attachType : 'auto'; - kmwOptions.root = 'source'; - kmwOptions.resources = 'source'; + + if(!kmwOptions.root) { + kmwOptions.root = 'source'; + } + + if(!kmwOptions.resources) { + kmwOptions.resources = '../../../../source'; + } if(ui) { var ui = setupScript('source/kmwui' + ui + '.js'); From fc19f326d160cfa570d44e9c85de6d1447819123 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 9 Feb 2018 16:20:01 +0700 Subject: [PATCH 02/26] Unit test can now simulate a key press. Desktop only; is not yet filtered from touch tests. --- web/unit_tests/cases/engine.js | 57 +++++++++++++++++++++++++++++++--- web/unit_tests/manual.conf.js | 2 ++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index 29d902d4681..a2ace49f137 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -12,8 +12,12 @@ describe('Engine', function() { initTimer(done); }); - beforeEach(function() { + beforeEach(function(done) { fixture.load("singleInput.html"); + + window.setTimeout(function() { + done() + }, 50); }); after(function() { @@ -24,17 +28,60 @@ describe('Engine', function() { fixture.cleanup(); }); - describe('Keyboards', function() { - it('Successfully loads a locally-stored keyboard.', function(done) { + describe('Keyboard Loading', function() { + it('Local', function(done) { var laoStub = fixture.load("/keyboards/lao_2008_basic.json", true); keyman.addKeyboards(laoStub); keyman.setActiveKeyboard("Keyboard_lao_2008_basic", "lao"); window.setTimeout(function() { - assert.isTrue(keyman.getActiveKeyboard() == "Keyboard_lao_2008_basic"); + assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic"); + + keyman.removeKeyboards('lao_2008_basic'); done(); - }, 1000); + }, 500); }); }); + + describe('Processing', function() { + before(function(done){ + var laoStub = fixture.load("/keyboards/lao_2008_basic.json", true); + + keyman.addKeyboards(laoStub); + keyman.setActiveKeyboard("Keyboard_lao_2008_basic", "lao"); + + window.setTimeout(function() { + done(); + }, 500); + }); + + after(function() { + keyman.removeKeyboards('lao_2008_basic'); + }) + + it('Simple Key', function(done) { + var inputElem = document.getElementById('singleton'); + inputElem.focus(); + + // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. + var event; + if(typeof Event == 'function') { + event = new Event("keydown", {"key":"s", "code":"KeyS", "keyCode":83, "which":83}); + event.keyCode = 83; + event.getModifierState = function() { + return false; + }; + } else { + event = document.createEvent("KeyboardEvent"); + event.initKeyboardEvent("keydown", false, true, null, String.fromCharCode(83), 0, 0, "", 0); + } + inputElem.dispatchEvent(event); + + window.setTimeout(function() { + assert.equal(inputElem.value, "ຫ"); + done(); + }, 50); + }); + }) }); \ No newline at end of file diff --git a/web/unit_tests/manual.conf.js b/web/unit_tests/manual.conf.js index 77cefec01a1..77dfc4d30ba 100644 --- a/web/unit_tests/manual.conf.js +++ b/web/unit_tests/manual.conf.js @@ -25,6 +25,8 @@ module.exports = function(config) { {pattern: 'release/unminified/web/**/*.css', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/**/*.gif', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/**/*.png', watched: false, served: true, included: false}, // OSK resources + {pattern: 'release/unminified/web/**/*.ttf', watched: false, served: true, included: false}, // OSK resources + {pattern: 'release/unminified/web/**/*.woff', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/*.js', watched: true, served: true, included: false}, // The actual KMW code. {pattern: 'release/unminified/web/*.map', watched: true, served: true, included: false}, // + sourcemaps. {pattern: 'unit_tests/fixtures/**/*.html', watched: true} // HTML structures useful for testing. From 317230b7e9aca41429eb2279f69aedbf622d7597 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 9 Feb 2018 16:21:36 +0700 Subject: [PATCH 03/26] Slight documentation enhancement. --- web/unit_tests/cases/engine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index a2ace49f137..7572577e509 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -8,7 +8,7 @@ describe('Engine', function() { fixture.setBase('unit_tests/fixtures'); setupKMW(); - // Pass the initTimer method ousr 'done' callback so it can handle our initialization delays for us. + // Pass the initTimer method our 'done' callback so it can handle our initialization delays for us. initTimer(done); }); @@ -72,7 +72,7 @@ describe('Engine', function() { event.getModifierState = function() { return false; }; - } else { + } else { // Yeah, so IE can't use the above at all, and requires its own trick. event = document.createEvent("KeyboardEvent"); event.initKeyboardEvent("keydown", false, true, null, String.fromCharCode(83), 0, 0, "", 0); } From 9b5ef1f7f82a799a74553f879875a8befccf04c3 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 9 Feb 2018 19:41:25 +0700 Subject: [PATCH 04/26] A click-simulating OSK test has been added to the engine suite. --- web/unit_tests/cases/engine.js | 49 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index 7572577e509..c7c0a6f9309 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -56,11 +56,17 @@ describe('Engine', function() { }, 500); }); + beforeEach(function() { + var inputElem = document.getElementById('singleton'); + inputElem.value = ""; + }); + after(function() { keyman.removeKeyboards('lao_2008_basic'); - }) + fixture.cleanup(); + }); - it('Simple Key', function(done) { + it('Simple Keypress', function() { var inputElem = document.getElementById('singleton'); inputElem.focus(); @@ -78,10 +84,47 @@ describe('Engine', function() { } inputElem.dispatchEvent(event); + assert.equal(inputElem.value, "ຫ"); + }); + + it('Simple OSK click', function(done) { + var inputElem = document.getElementById('singleton'); + + /* We hack KMW a little bit because the .focus method is insufficient; + * it won't trigger if the tested browser doesn't have focus. + * Only one can have focus when testing locally. + */ + DOMEventHandlers.states.lastActiveElement = inputElem; + + // Let the focus() method do its thing. window.setTimeout(function() { + var osk_S = document.getElementById('default-K_S'); + + // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. + var downEvent; + var upEvent; + if(typeof Event == 'function') { + downEvent = new Event("mousedown", {"relatedTarget": osk_S}); + upEvent = new Event("mouseup", {"relatedTarget": osk_S}); + } else { // Yeah, so IE can't use the above at all, and requires its own trick. + downEvent = document.createEvent("MouseEvent"); + downEvent.initMouseEvent("mousedown", false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, osk_S); + + upEvent = document.createEvent("MouseEvent"); + upEvent.initMouseEvent("mouseup", false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, osk_S); + } + osk_S.dispatchEvent(downEvent); + osk_S.dispatchEvent(upEvent); + assert.equal(inputElem.value, "ຫ"); done(); - }, 50); + }, 25); }); }) }); \ No newline at end of file From e58312dd8bd0f2a3bbfd2897797102278f096bcd Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 9 Feb 2018 19:42:22 +0700 Subject: [PATCH 05/26] Removed unnecessary timeout delays. --- web/unit_tests/cases/engine.js | 56 ++++++++++++++++------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index c7c0a6f9309..75ee67e7447 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -87,7 +87,7 @@ describe('Engine', function() { assert.equal(inputElem.value, "ຫ"); }); - it('Simple OSK click', function(done) { + it('Simple OSK click', function() { var inputElem = document.getElementById('singleton'); /* We hack KMW a little bit because the .focus method is insufficient; @@ -96,35 +96,31 @@ describe('Engine', function() { */ DOMEventHandlers.states.lastActiveElement = inputElem; - // Let the focus() method do its thing. - window.setTimeout(function() { - var osk_S = document.getElementById('default-K_S'); - - // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. - var downEvent; - var upEvent; - if(typeof Event == 'function') { - downEvent = new Event("mousedown", {"relatedTarget": osk_S}); - upEvent = new Event("mouseup", {"relatedTarget": osk_S}); - } else { // Yeah, so IE can't use the above at all, and requires its own trick. - downEvent = document.createEvent("MouseEvent"); - downEvent.initMouseEvent("mousedown", false, true, null, - null, 0, 0, 0, 0, - false, false, false, false, - 0, osk_S); - - upEvent = document.createEvent("MouseEvent"); - upEvent.initMouseEvent("mouseup", false, true, null, - null, 0, 0, 0, 0, - false, false, false, false, - 0, osk_S); - } - osk_S.dispatchEvent(downEvent); - osk_S.dispatchEvent(upEvent); - - assert.equal(inputElem.value, "ຫ"); - done(); - }, 25); + var osk_S = document.getElementById('default-K_S'); + + // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. + var downEvent; + var upEvent; + if(typeof Event == 'function') { + downEvent = new Event("mousedown", {"relatedTarget": osk_S}); + upEvent = new Event("mouseup", {"relatedTarget": osk_S}); + } else { // Yeah, so IE can't use the above at all, and requires its own trick. + downEvent = document.createEvent("MouseEvent"); + downEvent.initMouseEvent("mousedown", false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, osk_S); + + upEvent = document.createEvent("MouseEvent"); + upEvent.initMouseEvent("mouseup", false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, osk_S); + } + osk_S.dispatchEvent(downEvent); + osk_S.dispatchEvent(upEvent); + + assert.equal(inputElem.value, "ຫ"); }); }) }); \ No newline at end of file From 6d1f718d957eb81adb8ef31542a86f328b79cd98 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sat, 10 Feb 2018 16:32:34 +0700 Subject: [PATCH 06/26] Design and partial integration of recording-oriented resources for engine testing. --- web/testing/recorder/README.md | 1 + web/testing/recorder/build.sh | 52 ++++++++ web/testing/recorder/index.html | 129 ++++++++++++++++++ web/testing/recorder/keyboardScripts.js | 114 ++++++++++++++++ web/testing/recorder/source/inputEvents.ts | 145 +++++++++++++++++++++ web/testing/recorder/source/tsconfig.json | 9 ++ web/unit_tests/.gitignore | 1 + web/unit_tests/cases/engine.js | 55 ++------ web/unit_tests/manual.conf.js | 1 + 9 files changed, 462 insertions(+), 45 deletions(-) create mode 100644 web/testing/recorder/README.md create mode 100644 web/testing/recorder/build.sh create mode 100644 web/testing/recorder/index.html create mode 100644 web/testing/recorder/keyboardScripts.js create mode 100644 web/testing/recorder/source/inputEvents.ts create mode 100644 web/testing/recorder/source/tsconfig.json create mode 100644 web/unit_tests/.gitignore diff --git a/web/testing/recorder/README.md b/web/testing/recorder/README.md new file mode 100644 index 00000000000..6c240e787d7 --- /dev/null +++ b/web/testing/recorder/README.md @@ -0,0 +1 @@ +This subproject aims to facilitate engine debugging by using KeymanWeb to record input and output sequences for use in automated testing. \ No newline at end of file diff --git a/web/testing/recorder/build.sh b/web/testing/recorder/build.sh new file mode 100644 index 00000000000..bcde16f7c25 --- /dev/null +++ b/web/testing/recorder/build.sh @@ -0,0 +1,52 @@ +#! /bin/bash +# +# Compile keymanweb and copy compiled javascript and resources to output/embedded folder +# + +# Fails the build if a specified file does not exist. +assert ( ) { + if ! [ -f $1 ]; then + fail "Build failed." + exit 1 + fi +} + +fail() { + FAILURE_MSG="$1" + if [[ "$FAILURE_MSG" == "" ]]; then + FAILURE_MSG="Unknown failure" + fi + echo "${ERROR_RED}$FAILURE_MSG${NORMAL}" + exit 1 +} + +# Ensure the dependencies are downloaded. --no-optional should help block fsevents warnings. +echo "Node.js + dependencies check" +npm install --no-optional + +if [ $? -ne 0 ]; then + fail "Build environment setup error detected! Please ensure Node.js is installed!" +fi + +# Definition of global compile constants +OUTPUT="build" +NODE_SOURCE="testing/recorder/source" +ENGINE_TEST_OUTPUT="../../unit_tests/" + +readonly OUTPUT +readonly NODE_SOURCE +readonly ENGINE_TEST_OUTPUT + +# Ensures that we rely first upon the local npm-based install of Typescript. +# (Facilitates automated setup for build agents.) +PATH="../../node_modules/.bin:$PATH" + +compiler="npm run tsc --" +compilecmd="$compiler" + +$compilecmd -p $NODE_SOURCE/tsconfig.json +if [ $? -ne 0 ]; then + fail "Typescript compilation failed." +fi + +cp $OUTPUT/*.js $ENGINE_TEST_OUTPUT \ No newline at end of file diff --git a/web/testing/recorder/index.html b/web/testing/recorder/index.html new file mode 100644 index 00000000000..8c8bb043673 --- /dev/null +++ b/web/testing/recorder/index.html @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + KeymanWeb - Test Input Recorder + + + + + + + + + + + + + + + + + + + + + + + +

KeymanWeb - Test Input Recorder

+

This page is designed to record KeymanWeb input for use in testing.

+
+
+ +
+
+ +

Add a keyboard by keyboard name:

+ + + +

Add a keyboard by ISO 639 language code:

+ + + +

Add a keyboard by language name:

+ + +

+ Return to testing home page +

+ + + \ No newline at end of file diff --git a/web/testing/recorder/keyboardScripts.js b/web/testing/recorder/keyboardScripts.js new file mode 100644 index 00000000000..7512bde128e --- /dev/null +++ b/web/testing/recorder/keyboardScripts.js @@ -0,0 +1,114 @@ +// JavaScript Document keyboardScripts.js: Keyboard management for KeymanWeb demonstration pages + +/* + The keyboard name and/or ISO language code must be specified for each keyboard that is to be available. + If the same keyboard is used for several languages, it must be listed for each + language, but the keyboard itself will only be loaded once. + If two (or more) keyboards are to be available for a given language, both must be listed. + Any number of keyboards may be specified in one or more calls. + Keyboard paths may be absolute (with respect to the server root) or relative to the keyboards option path. + The actual keyboard object will be downloaded asynchronously when first selected for use. + + Each argument to addKeyboards() is a string, for example: + european2 loads the current version of the Eurolatin 2 keyboard (for its default language) + european2@fra loads the current version of the Eurolatin 2 keyboard for French + european2@fra@1.2 loads version 1.2 of the Eurolatin 2 keyboard for French + + Argument syntax also supports the following extensions: + @fra load the current version of the default keyboard for French + @fra$ load all available keyboards (current version) for French + + Each call to addKeyboards() requires a single call to the remote server, + (unless all keyboards listed are local and fully specified) so it is better + to use multiple arguments rather than separate function calls. + + Calling addKeyboards() with no arguments returns a list of *all* available keyboards. + The Toolbar (desktop browser) UI is best suited for allowing users to select + the appropriate language and keyboard in this case. + + Keyboards may also be specified by language name using addKeyboardsForLanguage() + for example: + keymanweb.addKeyboardsForLanguage('Burmese'); + + Appending $ to the language name will again cause all available keyboards for that + language to be loaded rather than the default keyboard. + + The first call to addKeyboardsForLanguage() makes an additional call to the + keyman API to load the current list of keyboard/language associations. + + In this example, the following function loads the indicated keyboards, + and is called when the page loads. +*/ + + function loadKeyboards() + { + var kmw=keyman; + + // The first keyboard added will be the default keyboard for touch devices. + // For faster loading, it may be best for the default keybaord to be + // locally sourced. + kmw.addKeyboards({id:'us',name:'English',languages:{id:'eng',name:'English'}, + filename:'../us-1.0.js'}); + + // Add more keyboards to the language menu, by keyboard name, + // keyboard name and language code, or just the ISO 639 language code. + // We use a different loading pattern here than in the samples version to provide a slightly different set of test cases. + kmw.addKeyboards('french','@heb'); + kmw.addKeyboards({id:'european2', name:'EuroLatin2', languages: [{id:'nor'}, {id:'swe'}]}); // Loads from partial stub instead of the compact string. + + // Add a keyboard by language name. Note that the name must be spelled + // correctly, or the keyboard will not be found. (Using ISO codes is + // usually easier.) + kmw.addKeyboardsForLanguage('Dzongkha'); + + // Add a fully-specified, locally-sourced, keyboard with custom font + kmw.addKeyboards({id:'lao_2008_basic',name:'Lao Basic', + languages:{ + id:'lao',name:'Lao',region:'Asia' + }, + filename:'../lao_2008_basic.js' + }); + + // The following two optional calls should be delayed until language menus are fully loaded: + // (a) a specific mapped input element input is focused, to ensure that the OSK appears + // (b) a specific keyboard is loaded, rather than the keyboard last used. + //window.setTimeout(function(){kmw.setActiveElement('ta1',true);},2500); + //window.setTimeout(function(){kmw.setActiveKeyboard('Keyboard_french','fra');},3000); + + // Note that locally specified keyboards will be listed before keyboards + // requested from the remote server by user interfaces that do not order + // keyboards alphabetically by language. + } + + // Script to allow a user to add any keyboard to the keyboard menu + function addKeyboard(n) + { + var sKbd,kmw=keyman; + switch(n) + { + case 1: + sKbd=document.getElementById('kbd_id1').value; + kmw.addKeyboards(sKbd); + break; + case 2: + sKbd=document.getElementById('kbd_id2').value.toLowerCase(); + var rx=new RegExp(/^\w{3,3}$\$?/); + if(rx.test(sKbd)) + kmw.addKeyboards('@'+sKbd); + else + alert('An ISO 639 language code must be exactly 3 letters long!'); + break; + case 3: + sKbd=document.getElementById('kbd_id3').value; + kmw.addKeyboardsForLanguage(sKbd); + break; + } + } + + // Add keyboard on Enter (as well as pressing button) + function clickOnEnter(e,id) + { + e = e || window.event; + if(e.keyCode == 13) addKeyboard(id); + } + diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts new file mode 100644 index 00000000000..717c34d48cc --- /dev/null +++ b/web/testing/recorder/source/inputEvents.ts @@ -0,0 +1,145 @@ +namespace KMWRecorder { + export class PhysicalInputEvent { + static readonly eventClass: string = "KeyboardEvent"; + static readonly eventType: string = "keydown"; + + static readonly modifierCodes: { [mod:string]: number } = { + "Shift":0x0001, + "Control":0x0002, + "Alt":0x0004, + "Meta":0x0008, + "CapsLock":0x0010, + "NumLock":0x0020, + "ScrollLock":0x0040 + }; + + // KeyboardEvent properties + type: string = "key"; + key: string; + code: string; + keyCode: number; + modifierSet: number; + location: number; + + constructor(e: KeyboardEvent|PhysicalInputEvent) { + // We condition upon newly-generated events, as a PhysicalInputEvent from JSON + // will lack its proper prototype, etc. + if(e instanceof KeyboardEvent) { + this.key = e.key; + this.code = e.code; + this.keyCode = e.keyCode; + this.modifierSet = this.compileModifierState(e); + this.location = e.location; + } else { + this.key = e.key; + this.code = e.code; + this.keyCode = e.keyCode; + this.modifierSet = e.modifierSet; + this.location = e.location; + } + } + + private compileModifierState(e: KeyboardEvent): number { + var flagSet: number = 0; + + for(var key in PhysicalInputEvent.modifierCodes) { + if(e.getModifierState(key)) { + flagSet |= PhysicalInputEvent.modifierCodes[key]; + } + } + + return flagSet; + } + + getModifierState(key: string): boolean { + return (PhysicalInputEvent.modifierCodes[key] & this.modifierSet) != 0; + } + + private generateModifierString(): string { + var list: string = ""; + + for(var key in PhysicalInputEvent.modifierCodes) { + if(this.getModifierState(key)) { + list += (key + list != "" ? " " : ""); + } + } + + return list; + } + + simulateEvent(ele: HTMLElement) { + var event: Event; + + // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. + var downEvent; + if(typeof Event == 'function') { + event = new Event(PhysicalInputEvent.eventType); + event['key'] = this.key; + event['code'] = this.code; + event['keyCode'] = this.keyCode; + event['location'] = this.location; + event['getModifierState'] = this.getModifierState.bind(this); + } else { // Yeah, so IE can't use the above at all, and requires its own trick. + event = document.createEvent(PhysicalInputEvent.eventClass); + // An override to ensure that IE's method gets called. + (event).initKeyboardEvent(PhysicalInputEvent.eventType, false, true, null, this.key, this.code, this.location, + this.generateModifierString(), 0); + } + + ele.dispatchEvent(event); + } + } + + export class OSKInputEvent { + static readonly eventClass: string = "MouseEvent"; + static readonly downEventType: string = "mousedown"; + static readonly upEventType: string = "mouseup"; + + type: string = "osk"; + keyID: string; + + // osk.clickKey receives the element clicked or touched in OSK interactions. + constructor(ele: HTMLDivElement|OSKInputEvent) { + if(ele instanceof HTMLDivElement) { + this.keyID = ele.id; + } else { + this.keyID = ele.keyID; + } + } + + simulateEvent(target: HTMLElement) { + var oskKeyElement = document.getElementById(this.keyID); + + // To be safe, we replicate the MouseEvent similarly to the keystroke event. + var downEvent; + var upEvent; + if(typeof Event == 'function') { + downEvent = new Event(OSKInputEvent.downEventType); + upEvent = new Event(OSKInputEvent.upEventType); + downEvent['relatedTarget'] = target; + upEvent['relatedTarget'] = target; + } else { // Yeah, so IE can't use the above at all, and requires its own trick. + downEvent = document.createEvent(OSKInputEvent.eventClass); + downEvent.initMouseEvent(OSKInputEvent.downEventType, false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, oskKeyElement); + + upEvent = document.createEvent(OSKInputEvent.eventClass); + upEvent.initMouseEvent(OSKInputEvent.upEventType, false, true, null, + null, 0, 0, 0, 0, + false, false, false, false, + 0, oskKeyElement); + } + + /* We hack KMW a little bit because the .focus method is insufficient; + * it won't trigger if the tested browser doesn't have focus. + * Only one can have focus when testing locally. + */ + window['DOMEventHandlers'].states.lastActiveElement = target; + + oskKeyElement.dispatchEvent(downEvent); + oskKeyElement.dispatchEvent(upEvent); + } + } +} \ No newline at end of file diff --git a/web/testing/recorder/source/tsconfig.json b/web/testing/recorder/source/tsconfig.json new file mode 100644 index 00000000000..8e2cd035331 --- /dev/null +++ b/web/testing/recorder/source/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, + "module": "none", + "outDir": "../build/", + "sourceMap": true, + "target": "es5" + } +} diff --git a/web/unit_tests/.gitignore b/web/unit_tests/.gitignore new file mode 100644 index 00000000000..00fa658edb2 --- /dev/null +++ b/web/unit_tests/.gitignore @@ -0,0 +1 @@ +inputEvents.js diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index 75ee67e7447..15e09e3fc2c 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -68,21 +68,11 @@ describe('Engine', function() { it('Simple Keypress', function() { var inputElem = document.getElementById('singleton'); - inputElem.focus(); - - // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. - var event; - if(typeof Event == 'function') { - event = new Event("keydown", {"key":"s", "code":"KeyS", "keyCode":83, "which":83}); - event.keyCode = 83; - event.getModifierState = function() { - return false; - }; - } else { // Yeah, so IE can't use the above at all, and requires its own trick. - event = document.createEvent("KeyboardEvent"); - event.initKeyboardEvent("keydown", false, true, null, String.fromCharCode(83), 0, 0, "", 0); - } - inputElem.dispatchEvent(event); + + var lao_s_key_json = {"key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}; + var lao_s_event = new KMWRecorder.PhysicalInputEvent(lao_s_key_json); + + lao_s_event.simulateEvent(inputElem); assert.equal(inputElem.value, "ຫ"); }); @@ -90,37 +80,12 @@ describe('Engine', function() { it('Simple OSK click', function() { var inputElem = document.getElementById('singleton'); - /* We hack KMW a little bit because the .focus method is insufficient; - * it won't trigger if the tested browser doesn't have focus. - * Only one can have focus when testing locally. - */ - DOMEventHandlers.states.lastActiveElement = inputElem; - - var osk_S = document.getElementById('default-K_S'); - - // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. - var downEvent; - var upEvent; - if(typeof Event == 'function') { - downEvent = new Event("mousedown", {"relatedTarget": osk_S}); - upEvent = new Event("mouseup", {"relatedTarget": osk_S}); - } else { // Yeah, so IE can't use the above at all, and requires its own trick. - downEvent = document.createEvent("MouseEvent"); - downEvent.initMouseEvent("mousedown", false, true, null, - null, 0, 0, 0, 0, - false, false, false, false, - 0, osk_S); - - upEvent = document.createEvent("MouseEvent"); - upEvent.initMouseEvent("mouseup", false, true, null, - null, 0, 0, 0, 0, - false, false, false, false, - 0, osk_S); - } - osk_S.dispatchEvent(downEvent); - osk_S.dispatchEvent(upEvent); + var lao_s_osk_json = {"keyID": 'shift-K_S'}; + var lao_s_event = new KMWRecorder.OSKInputEvent(lao_s_osk_json); - assert.equal(inputElem.value, "ຫ"); + lao_s_event.simulateEvent(inputElem); + + assert.equal(inputElem.value, ";"); }); }) }); \ No newline at end of file diff --git a/web/unit_tests/manual.conf.js b/web/unit_tests/manual.conf.js index 77dfc4d30ba..07b0c7f7339 100644 --- a/web/unit_tests/manual.conf.js +++ b/web/unit_tests/manual.conf.js @@ -19,6 +19,7 @@ module.exports = function(config) { files: [ 'unit_tests/test_utils.js', // A basic utility script useful for constructing tests 'unit_tests/modernizr.js', // A dependency-managed utility script that helps with browser feature detection. + 'unit_tests/inputEvents.js', // The object definitions used to generate/replicate key events for engine tests. 'unit_tests/cases/**/*.js', // Where the tests actually reside. 'unit_tests/json/**/*.json', // Where pre-loaded JSON resides. {pattern: 'unit_tests/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. From 12d3a046b67631b4fcaf8ab0fcf3d105d07583c1 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sat, 10 Feb 2018 21:37:34 +0700 Subject: [PATCH 07/26] Added new InputTestSequence class and integrated a test with it. --- web/testing/recorder/source/inputEvents.ts | 86 +++++++++++++++++++++- web/unit_tests/cases/engine.js | 51 ++++++++++++- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts index 717c34d48cc..44d5d2b2e2c 100644 --- a/web/testing/recorder/source/inputEvents.ts +++ b/web/testing/recorder/source/inputEvents.ts @@ -1,5 +1,21 @@ namespace KMWRecorder { - export class PhysicalInputEvent { + export abstract class InputEvent { + abstract simulateEventOn(ele: HTMLElement): void; + + static fromJSONObject(obj: any): InputEvent { + if(obj && obj.type) { + if(obj.type == "key") { + return new PhysicalInputEvent(obj); + } else if(obj.type == "osk") { + return new OSKInputEvent(obj); + } + } else { + throw new SyntaxError("Error in JSON format corresponding to an InputEvent!"); + } + } + } + + export class PhysicalInputEvent implements InputEvent { static readonly eventClass: string = "KeyboardEvent"; static readonly eventType: string = "keydown"; @@ -67,7 +83,7 @@ namespace KMWRecorder { return list; } - simulateEvent(ele: HTMLElement) { + simulateEventOn(ele: HTMLElement) { var event: Event; // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. @@ -90,7 +106,7 @@ namespace KMWRecorder { } } - export class OSKInputEvent { + export class OSKInputEvent implements InputEvent { static readonly eventClass: string = "MouseEvent"; static readonly downEventType: string = "mousedown"; static readonly upEventType: string = "mouseup"; @@ -107,7 +123,7 @@ namespace KMWRecorder { } } - simulateEvent(target: HTMLElement) { + simulateEventOn(target: HTMLElement) { var oskKeyElement = document.getElementById(this.keyID); // To be safe, we replicate the MouseEvent similarly to the keystroke event. @@ -142,4 +158,66 @@ namespace KMWRecorder { oskKeyElement.dispatchEvent(upEvent); } } + + export class InputTestSequence { + inputs: InputEvent[]; + output: string; + msg?: string; + + constructor(ins?: InputEvent[] | InputTestSequence, outs?: string, msg?: string) { + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); + } else { + // We're constructing from existing JSON. + this.inputs = []; + + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(InputEvent.fromJSONObject(ins.inputs[ie])); + } + + this.output = ins.output; + this.msg = ins.msg; + return; + } + } else { + this.inputs = []; + } + + if(outs) { + this.output = outs; + } + + if(msg) { + this.msg = msg; + } + } + + simulateSequenceOn(ele: HTMLElement, assertCallback: (s1: any, s2: any, msg?: string) => void): boolean { + if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { + window['keyman'].resetContext(); + ele.value = ""; + } else { + window['keyman'].resetContext(); + ele.textContent = ""; + } + + for(var i=0; i < this.inputs.length; i++) { + this.inputs[i].simulateEventOn(ele); + } + + var result; + if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { + result = ele.value; + } else { + result = ele.textContent; + } + + if(assertCallback) { + assertCallback(result, this.output, this.msg); + } + + return result == this.output; + } + } } \ No newline at end of file diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index 15e09e3fc2c..c479f228ec3 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -44,6 +44,8 @@ describe('Engine', function() { }); }); + // Performs basic processing system checks/tests to ensure the sequence testing + // is based on correct assumptions about the code. describe('Processing', function() { before(function(done){ var laoStub = fixture.load("/keyboards/lao_2008_basic.json", true); @@ -69,10 +71,10 @@ describe('Engine', function() { it('Simple Keypress', function() { var inputElem = document.getElementById('singleton'); - var lao_s_key_json = {"key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}; + var lao_s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}; var lao_s_event = new KMWRecorder.PhysicalInputEvent(lao_s_key_json); - lao_s_event.simulateEvent(inputElem); + lao_s_event.simulateEventOn(inputElem); assert.equal(inputElem.value, "ຫ"); }); @@ -80,12 +82,53 @@ describe('Engine', function() { it('Simple OSK click', function() { var inputElem = document.getElementById('singleton'); - var lao_s_osk_json = {"keyID": 'shift-K_S'}; + var lao_s_osk_json = {"type": "osk", "keyID": 'shift-K_S'}; var lao_s_event = new KMWRecorder.OSKInputEvent(lao_s_osk_json); - lao_s_event.simulateEvent(inputElem); + lao_s_event.simulateEventOn(inputElem); assert.equal(inputElem.value, ";"); }); + // TODO: add a 'resetContext' test! + }) + + describe('Sequence Testing', function() { + before(function(done){ + var laoStub = fixture.load("/keyboards/lao_2008_basic.json", true); + + keyman.addKeyboards(laoStub); + keyman.setActiveKeyboard("Keyboard_lao_2008_basic", "lao"); + + window.setTimeout(function() { + done(); + }, 500); + }); + + after(function() { + keyman.removeKeyboards('lao_2008_basic'); + fixture.cleanup(); + }); + + it('Keyboard simulation', function() { + var inputElem = document.getElementById('singleton'); + + var lao_s_key_json = { + "inputs": [{"type":"key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}], + "output": "ຫ" + }; + var lao_s_test = new KMWRecorder.InputTestSequence(lao_s_key_json); + lao_s_test.simulateSequenceOn(inputElem, assert.equal); + }); + + it('OSK simulation', function() { + var inputElem = document.getElementById('singleton'); + + var lao_s_osk_json = { + "inputs": [{"type":"osk", "keyID": 'shift-K_S'}], + "output": ";" + }; + var lao_s_test = new KMWRecorder.InputTestSequence(lao_s_osk_json); + lao_s_test.simulateSequenceOn(inputElem, assert.equal); + }); }) }); \ No newline at end of file From 1f35e7a1f6037768a8670e519844cbb013a075fe Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 00:00:31 +0700 Subject: [PATCH 08/26] Partial touch support - keystrokes on touch-aliased elements are now testable. Problem: touch events. --- web/testing/recorder/source/inputEvents.ts | 23 +++++++++++++++------- web/unit_tests/cases/engine.js | 18 +++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts index 44d5d2b2e2c..8fe395a3c16 100644 --- a/web/testing/recorder/source/inputEvents.ts +++ b/web/testing/recorder/source/inputEvents.ts @@ -159,6 +159,21 @@ namespace KMWRecorder { } } + var resetElement = function(ele: HTMLElement):void { + if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { + window['keyman'].resetContext(); + ele.value = ""; + } else { + window['keyman'].resetContext(); + if(ele['base']) { + // Gotta be extra-careful with the simulated touch fields! + window['keyman'].touchAliasing.setText(ele, "", 0); + } else { + ele.textContent = ""; + } + } + } + export class InputTestSequence { inputs: InputEvent[]; output: string; @@ -194,13 +209,7 @@ namespace KMWRecorder { } simulateSequenceOn(ele: HTMLElement, assertCallback: (s1: any, s2: any, msg?: string) => void): boolean { - if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { - window['keyman'].resetContext(); - ele.value = ""; - } else { - window['keyman'].resetContext(); - ele.textContent = ""; - } + resetElement(ele); for(var i=0; i < this.inputs.length; i++) { this.inputs[i].simulateEventOn(ele); diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index c479f228ec3..c7e593eafe2 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -70,23 +70,35 @@ describe('Engine', function() { it('Simple Keypress', function() { var inputElem = document.getElementById('singleton'); + if(inputElem['kmw_ip']) { + inputElem = inputElem['kmw_ip']; + } var lao_s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}; var lao_s_event = new KMWRecorder.PhysicalInputEvent(lao_s_key_json); lao_s_event.simulateEventOn(inputElem); + if(inputElem['base']) { + inputElem = inputElem['base']; + } assert.equal(inputElem.value, "ຫ"); }); it('Simple OSK click', function() { var inputElem = document.getElementById('singleton'); + if(inputElem['kmw_ip']) { + inputElem = inputElem['kmw_ip']; + } var lao_s_osk_json = {"type": "osk", "keyID": 'shift-K_S'}; var lao_s_event = new KMWRecorder.OSKInputEvent(lao_s_osk_json); lao_s_event.simulateEventOn(inputElem); + if(inputElem['base']) { + inputElem = inputElem['base']; + } assert.equal(inputElem.value, ";"); }); // TODO: add a 'resetContext' test! @@ -111,6 +123,9 @@ describe('Engine', function() { it('Keyboard simulation', function() { var inputElem = document.getElementById('singleton'); + if(inputElem['kmw_ip']) { + inputElem = inputElem['kmw_ip']; + } var lao_s_key_json = { "inputs": [{"type":"key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}], @@ -122,6 +137,9 @@ describe('Engine', function() { it('OSK simulation', function() { var inputElem = document.getElementById('singleton'); + if(inputElem['kmw_ip']) { + inputElem = inputElem['kmw_ip']; + } var lao_s_osk_json = { "inputs": [{"type":"osk", "keyID": 'shift-K_S'}], From b493be289e8300e720bde174d7f3eb6ce5685624 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 01:03:37 +0700 Subject: [PATCH 09/26] Touch emulation now supported. Had to alter true code to run dev tests in Chrome emulation. --- web/source/kmwosk.ts | 7 +++--- web/testing/recorder/source/inputEvents.ts | 27 +++++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/web/source/kmwosk.ts b/web/source/kmwosk.ts index 2ea7d00eeaf..b6561f1c3e8 100644 --- a/web/source/kmwosk.ts +++ b/web/source/kmwosk.ts @@ -2098,10 +2098,11 @@ if(!window['keyman']['initialized']) { objectWidth = osk.getWidth(); } - if(device.touchable && ('ontouchstart' in window)) - { + if(device.touchable) // /*&& ('ontouchstart' in window)*/ // Except Chrome emulation doesn't set this. + { // Not to mention, it's rather redundant. lDiv.addEventListener('touchstart', osk.touch,true); - lDiv.addEventListener('touchend', osk.release,false); + // The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'. + lDiv.addEventListener('touchend', osk.release,true); lDiv.addEventListener('touchmove', osk.moveOver,false); //lDiv.addEventListener('touchcancel', osk.cancel,false); //event never generated by iOS } diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts index 8fe395a3c16..1099ff28856 100644 --- a/web/testing/recorder/source/inputEvents.ts +++ b/web/testing/recorder/source/inputEvents.ts @@ -108,8 +108,10 @@ namespace KMWRecorder { export class OSKInputEvent implements InputEvent { static readonly eventClass: string = "MouseEvent"; - static readonly downEventType: string = "mousedown"; - static readonly upEventType: string = "mouseup"; + static readonly downMouseType: string = "mousedown"; + static readonly upMouseType: string = "mouseup"; + static readonly downTouchType: string = "touchstart"; + static readonly upTouchType: string = "touchend"; type: string = "osk"; keyID: string; @@ -130,19 +132,28 @@ namespace KMWRecorder { var downEvent; var upEvent; if(typeof Event == 'function') { - downEvent = new Event(OSKInputEvent.downEventType); - upEvent = new Event(OSKInputEvent.upEventType); - downEvent['relatedTarget'] = target; - upEvent['relatedTarget'] = target; + if(target['base'] && target instanceof HTMLDivElement) { + downEvent = new Event(OSKInputEvent.downTouchType); + upEvent = new Event(OSKInputEvent.upTouchType); + downEvent['touches'] = [{"target": oskKeyElement}]; + upEvent['touches'] = [{"target": oskKeyElement}]; + downEvent['changedTouches'] = [{"target": oskKeyElement}]; + upEvent['changedTouches'] = [{"target": oskKeyElement}]; + } else { + downEvent = new Event(OSKInputEvent.downMouseType); + upEvent = new Event(OSKInputEvent.upMouseType); + downEvent['relatedTarget'] = target; + upEvent['relatedTarget'] = target; + } } else { // Yeah, so IE can't use the above at all, and requires its own trick. downEvent = document.createEvent(OSKInputEvent.eventClass); - downEvent.initMouseEvent(OSKInputEvent.downEventType, false, true, null, + downEvent.initMouseEvent(OSKInputEvent.downMouseType, false, true, null, null, 0, 0, 0, 0, false, false, false, false, 0, oskKeyElement); upEvent = document.createEvent(OSKInputEvent.eventClass); - upEvent.initMouseEvent(OSKInputEvent.upEventType, false, true, null, + upEvent.initMouseEvent(OSKInputEvent.upMouseType, false, true, null, null, 0, 0, 0, 0, false, false, false, false, 0, oskKeyElement); From 1b2da39e1bb678ad9fc4438f03af8c2766066877 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 12:56:45 +0700 Subject: [PATCH 10/26] Added code to capture + reconstruct keyboard stubs from KMW's activeStub property. --- web/testing/recorder/index.html | 9 ++++ web/testing/recorder/keyboardScripts.js | 2 +- web/testing/recorder/source/inputEvents.ts | 60 ++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/web/testing/recorder/index.html b/web/testing/recorder/index.html index 8c8bb043673..5df5b2efae2 100644 --- a/web/testing/recorder/index.html +++ b/web/testing/recorder/index.html @@ -96,6 +96,15 @@ return _ock(e); } + var _sak = keyman.keyboardManager._SetActiveKeyboard.bind(keyman.keyboardManager); + keyman.keyboardManager._SetActiveKeyboard = function(PInternalName, PLgCode, saveCookie) { + _sak(PInternalName, PLgCode, saveCookie); + + // What's the active stub immediately after our _SetActiveKeyboard call? + var kbdRecord = new KMWRecorder.KeyboardStub(keyman.keyboardManager.activeStub); + console.log(JSON.stringify(kbdRecord)); + } + diff --git a/web/testing/recorder/keyboardScripts.js b/web/testing/recorder/keyboardScripts.js index 7512bde128e..0e0274f840d 100644 --- a/web/testing/recorder/keyboardScripts.js +++ b/web/testing/recorder/keyboardScripts.js @@ -64,7 +64,7 @@ // Add a fully-specified, locally-sourced, keyboard with custom font kmw.addKeyboards({id:'lao_2008_basic',name:'Lao Basic', languages:{ - id:'lao',name:'Lao',region:'Asia' + id:'lao',name:'Lao',region:'Asia', }, filename:'../lao_2008_basic.js' }); diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts index 1099ff28856..b874c1bd223 100644 --- a/web/testing/recorder/source/inputEvents.ts +++ b/web/testing/recorder/source/inputEvents.ts @@ -240,4 +240,64 @@ namespace KMWRecorder { return result == this.output; } } + + class FontStubForLanguage { + family: string; + source: string[]; + + constructor(activeStubEntry: any) { + this.family = activeStubEntry.family; + + var src = activeStubEntry.files; + if(!(src instanceof Array)) { + src = [ src ]; + } + + this.source = []; + for(var i=0; i < src.length; i++) { + this.source.push(activeStubEntry.path + src[i]); + } + } + } + + class LanguageStubForKeyboard { + id: string; + name: string; + region: string; + font?: FontStubForLanguage; + oskFont?: FontStubForLanguage; + + constructor(activeStub: any) { + this.id = activeStub.KLC; + this.name = activeStub.KL; + this.region = activeStub.KR; + + // Fonts. + if(activeStub.KFont) { + this.font = new FontStubForLanguage(activeStub.KFont); + } + if(activeStub.KOskFont) { + this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + } + } + } + + export class KeyboardStub { + id: string; + name: string; + filename: string; + languages: LanguageStubForKeyboard[]; + + // Constructs a stub usable with KeymanWeb's addKeyboards() API function from + // the internally-tracked ActiveStub value for that keyboard. + constructor(activeStub: any) { + this.id = activeStub.KI; + this.id = this.id.replace('Keyboard_', ''); + + this.name = activeStub.KN; + this.filename = activeStub.KF; + + this.languages = [new LanguageStubForKeyboard(activeStub)]; + } + } } \ No newline at end of file From b3896bd1c01d48362571fba488f775b9809b8a58 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 14:36:08 +0700 Subject: [PATCH 11/26] Rudimentary test recording UI is now up for the recorder page. --- web/testing/recorder/index.html | 98 ++++++++++++++++++++-- web/testing/recorder/keyboardScripts.js | 4 + web/testing/recorder/source/inputEvents.ts | 5 ++ 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/web/testing/recorder/index.html b/web/testing/recorder/index.html index 5df5b2efae2..d362d4ca8aa 100644 --- a/web/testing/recorder/index.html +++ b/web/testing/recorder/index.html @@ -79,21 +79,67 @@ @@ -115,10 +175,32 @@

KeymanWeb - Test Input Recorder

This page is designed to record KeymanWeb input for use in testing.


- +
+

Recorded output JSON:

+ +

You must click 'Set Sequence' to save any changes to a sequence before continuing input recording.

+ + + +
+
+
+

Record input here:

+ +
+
+
+

Currently active stub (for use with addKeyboards):

+ +

+ +

Add a keyboard by keyboard stub (to addKeyboards):

+
+ +

Add a keyboard by keyboard name:

diff --git a/web/testing/recorder/keyboardScripts.js b/web/testing/recorder/keyboardScripts.js index 0e0274f840d..bcd05c266b9 100644 --- a/web/testing/recorder/keyboardScripts.js +++ b/web/testing/recorder/keyboardScripts.js @@ -102,6 +102,10 @@ sKbd=document.getElementById('kbd_id3').value; kmw.addKeyboardsForLanguage(sKbd); break; + case 4: + sKbd=document.getElementById('kbd_stub_add').value; + kmw.addKeyboards(JSON.parse(sKbd)); + break; } } diff --git a/web/testing/recorder/source/inputEvents.ts b/web/testing/recorder/source/inputEvents.ts index b874c1bd223..b677f7fc52b 100644 --- a/web/testing/recorder/source/inputEvents.ts +++ b/web/testing/recorder/source/inputEvents.ts @@ -219,6 +219,11 @@ namespace KMWRecorder { } } + addInput(event: InputEvent, output: string) { + this.inputs.push(event); + this.output = output; + } + simulateSequenceOn(ele: HTMLElement, assertCallback: (s1: any, s2: any, msg?: string) => void): boolean { resetElement(ele); From a90cb142fa8a061902eef158b834baeb03d4d8b4 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 15:03:26 +0700 Subject: [PATCH 12/26] Recorder page is now Chrome-touch-simulation compatible. --- web/testing/recorder/index.html | 44 ++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/web/testing/recorder/index.html b/web/testing/recorder/index.html index d362d4ca8aa..0fc0e4f150b 100644 --- a/web/testing/recorder/index.html +++ b/web/testing/recorder/index.html @@ -84,39 +84,55 @@ var ta_inputJSON; var in_output; + setElementText = function(ele, text) { + ele.value = text; + if(ele['kmw_ip']) { + keyman.touchAliasing.setTextBeforeCaret(ele['kmw_ip'], ele.value); + } + } + addInputRecord = function(json) { inputJSON.addInput(json, in_output.value); - ta_inputJSON.value = JSON.stringify(inputJSON); + setElementText(ta_inputJSON, JSON.stringify(inputJSON)); } resetInputRecord = function() { - ta_inputJSON.value = ""; - in_output.value = ""; + setElementText(ta_inputJSON, ""); + setElementText(in_output, ""); inputJSON = new KMWRecorder.InputTestSequence(); } copyInputRecord = function() { try { - ta_inputJSON.select(); + if(!ta_inputJSON['kmw_ip']) { + ta_inputJSON.select(); + } else { + var range = document.createRange(); + range.selectNode(ta_inputJSON['kmw_ip']); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + } + var res = document.execCommand('copy'); if(res) { in_output.focus(); return; } - } catch (err) { } + } catch (err) { console.log(err) } alert("Unable to copy successfully."); } setInputRecord = function() { inputJSON = new KMWRecorder.InputTestSequence(JSON.parse(ta_inputJSON.value)); - in_output.value = inputJSON.output; + setElementText(in_output, inputJSON.output) } // Time for the 'magic'. Yay, JavaScript method extension strategies... var _kd = keyman.touchAliasing._KeyDown.bind(keyman.touchAliasing); keyman.touchAliasing._KeyDown = function(e) { - if(DOMEventHandlers.states.activeElement != in_output) { + if(DOMEventHandlers.states.activeElement != in_output && + DOMEventHandlers.states.activeElement != in_output['kmw_ip']) { return _kd(e); } @@ -130,8 +146,9 @@ var _ock = keyman.osk.clickKey.bind(keyman.osk); keyman.osk.clickKey = function(e) { - if(DOMEventHandlers.states.activeElement != in_output) { - return _kd(e); + if(DOMEventHandlers.states.activeElement != in_output && + DOMEventHandlers.states.activeElement != in_output['kmw_ip']) { + return _ock(e); } var event = new KMWRecorder.OSKInputEvent(e); @@ -148,22 +165,24 @@ // What's the active stub immediately after our _SetActiveKeyboard call? var internalStub = keyman.keyboardManager.activeStub; - if(internalStub && DOMEventHandlers.states.activeElement == in_output) { + if(internalStub && (DOMEventHandlers.states.activeElement == in_output + || DOMEventHandlers.states.activeElement == in_output['kmw_ip'])) { var kbdRecord = new KMWRecorder.KeyboardStub(internalStub); var ta_activeStub = document.getElementById('activeStub'); ta_activeStub.value = JSON.stringify(kbdRecord); - - console.log(JSON.stringify(kbdRecord)); } } window.addEventListener('load', function() { ta_inputJSON = document.getElementById('inputRecord'); in_output = document.getElementById('receiver'); + p_layout = document.getElementById('layout'); keyman.attachToControl(in_output); keyman.setKeyboardForControl(in_output, '', ''); resetInputRecord(); + + p_layout.textContent += keyman.util.device.formFactor; }); @@ -176,6 +195,7 @@

KeymanWeb - Test Input Recorder


+

Current layout:

Recorded output JSON:

You must click 'Set Sequence' to save any changes to a sequence before continuing input recording.

From 889d2e24e8afe4d6d1e5de508e759ae62844fea4 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Sun, 11 Feb 2018 15:34:49 +0700 Subject: [PATCH 13/26] Adjusts the OSK to allow the recorder to detect touch-based deletions. --- web/source/kmwosk.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/web/source/kmwosk.ts b/web/source/kmwosk.ts index b6561f1c3e8..f7d038fac14 100644 --- a/web/source/kmwosk.ts +++ b/web/source/kmwosk.ts @@ -956,9 +956,12 @@ if(!window['keyman']['initialized']) { keymanweb.domManager.initActiveElement(Lelem); // Exclude menu and OSK hide keys from normal click processing - if(keyName == 'K_LOPT' || keyName == 'K_ROPT') - { - osk.optionKey(e,keyName,true); return true; + if(keyName == 'K_LOPT' || keyName == 'K_ROPT') { + osk.optionKey(e,keyName,true); + return true; + } else if(keyName == 'K_BKSP') { + kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); + return true; } // Turn off key highlighting (or preview) @@ -2484,7 +2487,10 @@ if(!window['keyman']['initialized']) { } // Also backspace, to allow delete to repeat while key held else if(keyName == 'K_BKSP') { - kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); + // While we could inline the execution of the delete key here, we lose the ability to + // record the backspace key if we do so. + osk.clickKey(key); + osk.deleteKey = key; osk.deleting = window.setTimeout(osk.repeatDelete,500); osk.keyPending = null; } else { @@ -2791,7 +2797,7 @@ if(!window['keyman']['initialized']) { **/ osk.repeatDelete = function() { if(osk.deleting) { - kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); + osk.clickKey(osk.deleteKey); osk.deleting = window.setTimeout(osk.repeatDelete,100); } } From c0b518e5499337d12a63ac2cf6df5578f3427390 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 13 Feb 2018 08:46:05 +0700 Subject: [PATCH 14/26] Accidental lines left out of merge + necessitated change to the new 'engine' test case. --- web/unit_tests/base.conf.js | 9 +++++++++ web/unit_tests/cases/engine.js | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/web/unit_tests/base.conf.js b/web/unit_tests/base.conf.js index cba802d6e7b..aa2df51bfc8 100644 --- a/web/unit_tests/base.conf.js +++ b/web/unit_tests/base.conf.js @@ -15,7 +15,9 @@ module.exports = { files: [ 'unit_tests/test_utils.js', // A basic utility script useful for constructing tests 'unit_tests/modernizr.js', // A dependency-managed utility script that helps with browser feature detection. + 'unit_tests/inputEvents.js', // The object definitions used to generate/replicate key events for engine tests. 'unit_tests/cases/**/*.js', // Where the tests actually reside. + 'unit_tests/json/**/*.json', // Where pre-loaded JSON resides. {pattern: 'unit_tests/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. {pattern: 'release/unminified/web/**/*.css', watched: false, served: true, included: false}, // OSK resources {pattern: 'release/unminified/web/**/*.gif', watched: false, served: true, included: false}, // OSK resources @@ -42,6 +44,13 @@ module.exports = { // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'unit_tests/fixtures/**/*.html' : ['html2js'], + 'unit_tests/json/**/*.json' : ['json_fixtures'] + }, + + // Settings to properly configure how JSON fixtures are automatically loaded by Karma. + jsonFixturesPreprocessor: { + stripPrefix: 'unit_tests/json', + variableName: '__json__' }, // web server port diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index c7e593eafe2..8cb18522bd0 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -6,10 +6,7 @@ describe('Engine', function() { this.timeout(10000); fixture.setBase('unit_tests/fixtures'); - setupKMW(); - - // Pass the initTimer method our 'done' callback so it can handle our initialization delays for us. - initTimer(done); + setupKMW(null, done, 10000); }); beforeEach(function(done) { From 6970c8647747dd7336efdf5f9d89961b707487fe Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 13 Feb 2018 09:46:23 +0700 Subject: [PATCH 15/26] Relocates the recorder into the general /source/ directory + ties its build into the test command. --- .../recorder/build.sh => source/build_recorder.sh} | 12 +++++++----- .../recorder/index.html => source/kmwRecorder.html} | 12 ++++++------ web/source/kmwdom.ts | 4 ++-- web/source/kmwdomevents.ts | 4 ++++ web/source/kmwkeyboards.ts | 4 ++++ web/source/kmwosk.ts | 7 +++++-- .../recorder_InputEvents.ts} | 0 .../recorder_KeyboardScripts.js} | 0 web/source/tsconfig.recorder.json | 12 ++++++++++++ web/testing/recorder/README.md | 1 - web/testing/recorder/source/tsconfig.json | 9 --------- web/unit_tests/.gitignore | 2 +- web/unit_tests/base.conf.js | 3 ++- web/unit_tests/test.sh | 9 +++++++-- 14 files changed, 50 insertions(+), 29 deletions(-) rename web/{testing/recorder/build.sh => source/build_recorder.sh} (80%) rename web/{testing/recorder/index.html => source/kmwRecorder.html} (93%) rename web/{testing/recorder/source/inputEvents.ts => source/recorder_InputEvents.ts} (100%) rename web/{testing/recorder/keyboardScripts.js => source/recorder_KeyboardScripts.js} (100%) create mode 100644 web/source/tsconfig.recorder.json delete mode 100644 web/testing/recorder/README.md delete mode 100644 web/testing/recorder/source/tsconfig.json diff --git a/web/testing/recorder/build.sh b/web/source/build_recorder.sh similarity index 80% rename from web/testing/recorder/build.sh rename to web/source/build_recorder.sh index bcde16f7c25..c9301961487 100644 --- a/web/testing/recorder/build.sh +++ b/web/source/build_recorder.sh @@ -29,9 +29,10 @@ if [ $? -ne 0 ]; then fi # Definition of global compile constants -OUTPUT="build" -NODE_SOURCE="testing/recorder/source" -ENGINE_TEST_OUTPUT="../../unit_tests/" +COMPILED_FILE="recorder_InputEvents.js" +OUTPUT="../release/recorder" +NODE_SOURCE="source" +ENGINE_TEST_OUTPUT="../unit_tests/" readonly OUTPUT readonly NODE_SOURCE @@ -44,9 +45,10 @@ PATH="../../node_modules/.bin:$PATH" compiler="npm run tsc --" compilecmd="$compiler" -$compilecmd -p $NODE_SOURCE/tsconfig.json +$compilecmd -p $NODE_SOURCE/tsconfig.recorder.json if [ $? -ne 0 ]; then fail "Typescript compilation failed." fi -cp $OUTPUT/*.js $ENGINE_TEST_OUTPUT \ No newline at end of file +cp $OUTPUT/$COMPILED_FILE $ENGINE_TEST_OUTPUT +cp $OUTPUT/$COMPILED_FILE.map $ENGINE_TEST_OUTPUT \ No newline at end of file diff --git a/web/testing/recorder/index.html b/web/source/kmwRecorder.html similarity index 93% rename from web/testing/recorder/index.html rename to web/source/kmwRecorder.html index 0fc0e4f150b..f30e25edd97 100644 --- a/web/testing/recorder/index.html +++ b/web/source/kmwRecorder.html @@ -42,7 +42,7 @@ - + - + - + - + - + @@ -195,7 +202,7 @@

KeymanWeb - Test Input Recorder


-

Current layout:

+

Active device information:

Recorded output JSON:

You must click 'Set Sequence' to save any changes to an input sequence above before continuing input recording, or they will be lost upon new input.

diff --git a/web/source/kmwapi.ts b/web/source/kmwapi.ts index a74999f60aa..d1311eb815e 100644 --- a/web/source/kmwapi.ts +++ b/web/source/kmwapi.ts @@ -36,7 +36,7 @@ goog.exportSymbol("Util.prototype.getAbsoluteX", Util.prototype._GetAbsoluteX); goog.exportSymbol("Util.prototype.getAbsoluteY", Util.prototype._GetAbsoluteY); goog.exportSymbol("Util.prototype.getAbsolute", Util.prototype._GetAbsolute); goog.exportSymbol("Util.prototype.createElement", Util.prototype.createElement); -goog.exportSymbol("Util.prototype.getIEVersion", Util.prototype._GetIEVersion); +goog.exportSymbol("Util.prototype.getIEVersion", Util.prototype.getIEVersion); goog.exportSymbol("Util.prototype.isTouchDevice", Util.prototype.isTouchDevice); goog.exportSymbol("Util.prototype.createShim", Util.prototype.createShim); goog.exportSymbol("Util.prototype.showShim", Util.prototype.showShim); diff --git a/web/source/kmwcallback.ts b/web/source/kmwcallback.ts index 66a42b19678..0349c38f359 100644 --- a/web/source/kmwcallback.ts +++ b/web/source/kmwcallback.ts @@ -880,7 +880,7 @@ class KeyboardInterface { * Description Get start of selection (with supplementary plane modifications) */ _SelPos(Pelem: HTMLElement) { - var Ldoc: Document, Ldv: Window, isMSIE=(this.keymanweb.util._GetIEVersion()<999); // I3363 (Build 301) + var Ldoc: Document, Ldv: Window, isMSIE=(Device._GetIEVersion()<999); // I3363 (Build 301) if((this.keymanweb).isPositionSynthesized()) return this.keymanweb.touchAliasing.getTextCaret(Pelem); diff --git a/web/source/kmwdevice.ts b/web/source/kmwdevice.ts new file mode 100644 index 00000000000..43bef4f1a2c --- /dev/null +++ b/web/source/kmwdevice.ts @@ -0,0 +1,165 @@ +// The Device object definition ------------------------------------------------- + +class Device { + touchable: boolean; + OS: string; + formFactor: string; + dyPortrait: number; + dyLandscape: number; + version: string; + orientation: string|number; + browser: string; + + // Generates a default Device value. + constructor() { + this.touchable = !!('ontouchstart' in window); + this.OS = ''; + this.formFactor='desktop'; + this.dyPortrait=0; + this.dyLandscape=0; + this.version='0'; + this.orientation=window.orientation; + this.browser=''; + } + + /** + * Get device horizontal DPI for touch devices, to set actual size of active regions + * Note that the actual physical DPI may be somewhat different. + * + * @return {number} + */ + getDPI(): number { + var t=document.createElement('DIV') ,s=t.style,dpi=96; + if(document.readyState !== 'complete') { + return dpi; + } + + t.id='calculateDPI'; + s.position='absolute'; s.display='block';s.visibility='hidden'; + s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px'; + document.body.appendChild(t); + dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio; + document.body.removeChild(t); + return dpi; + } + + detect() : void { + var IEVersion = Device._GetIEVersion(); + + if(navigator && navigator.userAgent) { + var agent=navigator.userAgent; + + if(agent.indexOf('iPad') >= 0) { + this.OS='iOS'; + this.formFactor='tablet'; + this.dyPortrait=this.dyLandscape=0; + } + + if(agent.indexOf('iPhone') >= 0) { + this.OS='iOS'; + this.formFactor='phone'; + this.dyPortrait=this.dyLandscape=25; + } + + if(agent.indexOf('Android') >= 0) { + this.OS='Android'; + this.formFactor='phone'; // form factor may be redefined on initialization + this.dyPortrait=75; + this.dyLandscape=25; + try { + var rx=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)"); + this.version=agent.match(rx)[1]; + } catch(ex) {} + } + if(agent.indexOf('Windows NT') >= 0) { + this.OS='Windows'; + if(agent.indexOf('Touch') >= 0) { + this.formFactor='phone'; // will be redefined as tablet if resolution high enough + } + + // Windows Phone and Tablet PC + if(typeof navigator.msMaxTouchPoints == 'number' && navigator.msMaxTouchPoints > 0) { + this.touchable=true; + } + } + } + + // var sxx=device.formFactor; + // Check and possibly revise form factor according to actual screen size (adjusted for Galaxy S, maybe OK generally?) + if(this.formFactor == 'tablet' && Math.min(screen.width,screen.height) < 400) { + this.formFactor='phone'; + } + + if(this.formFactor == 'phone' && Math.max(screen.width,screen.height) > 720) { + this.formFactor='tablet'; + } + + // alert(sxx+'->'+device.formFactor); + // Check for phony iOS devices (Win32 test excludes Chrome touch emulation on Windows)! + if(this.OS == 'iOS' && !('ongesturestart' in window) && navigator.platform != 'Win32') { + this.OS='Android'; + } + + // Determine application or browser + this.browser='web'; + if(IEVersion < 999) { + this.browser='ie'; + } else { + if(this.OS == 'iOS' || this.OS.toLowerCase() == 'macosx') { + this.browser='safari'; + } + + var bMatch=/Firefox|Chrome|OPR/; + if(bMatch.test(navigator.userAgent)) { + if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen)) { + this.browser='firefox'; + } else if(navigator.userAgent.indexOf('OPR') >= 0) { + this.browser='opera'; + } else if(navigator.userAgent.indexOf('Chrome') >= 0) { + this.browser='chrome'; + } + } + } + } + + static _GetIEVersion() { + var n, agent=''; + + if('userAgent' in navigator) { + agent=navigator.userAgent; + } + + // Test first for old versions + if('selection' in document) { // only defined for IE and not for IE 11!!! + var appVer=navigator.appVersion; + n=appVer.indexOf('MSIE '); + if(n >= 0) { + // Check for quirks mode page, always return 6 if so + if((document as Document).compatMode == 'BackCompat') { + return 6; + } + + appVer=appVer.substr(n+5); + n=appVer.indexOf('.'); + if(n > 0) { + return parseInt(appVer.substr(0,n),10); + } + } + } + + // Finally test for IE 11 (and later?) + n=agent.indexOf('Trident/'); + if(n < 0) { + return 999; + } + + agent=agent.substr(n+8); + n=agent.indexOf('.'); + if(n > 0){ + return parseInt(agent.substr(0,n),10)+4; + } + + return 999; + } +} + diff --git a/web/source/kmwutils.ts b/web/source/kmwutils.ts index 16cf3ff62c2..3902ae386bd 100644 --- a/web/source/kmwutils.ts +++ b/web/source/kmwutils.ts @@ -1,130 +1,7 @@ // Includes KMW-added property declaration extensions for HTML elements. /// - -// The Device object definition ------------------------------------------------- - -class Device { - touchable: boolean; - OS: string; - formFactor: string; - dyPortrait: number; - dyLandscape: number; - version: string; - orientation: string|number; - browser: string; - - // Generates a default Device value. - constructor() { - this.touchable = !!('ontouchstart' in window); - this.OS = ''; - this.formFactor='desktop'; - this.dyPortrait=0; - this.dyLandscape=0; - this.version='0'; - this.orientation=window.orientation; - this.browser=''; - } - - /** - * Get device horizontal DPI for touch devices, to set actual size of active regions - * Note that the actual physical DPI may be somewhat different. - * - * @return {number} - */ - getDPI(): number { - var t=document.createElement('DIV') ,s=t.style,dpi=96; - if(document.readyState !== 'complete') { - return dpi; - } - - t.id='calculateDPI'; - s.position='absolute'; s.display='block';s.visibility='hidden'; - s.left='10px'; s.top='10px'; s.width='1in'; s.height='10px'; - document.body.appendChild(t); - dpi=(typeof window.devicePixelRatio == 'undefined') ? t.offsetWidth : t.offsetWidth * window.devicePixelRatio; - document.body.removeChild(t); - return dpi; - } - - detect(IEVersion: number) : void { - if(navigator && navigator.userAgent) { - var agent=navigator.userAgent; - - if(agent.indexOf('iPad') >= 0) { - this.OS='iOS'; - this.formFactor='tablet'; - this.dyPortrait=this.dyLandscape=0; - } - - if(agent.indexOf('iPhone') >= 0) { - this.OS='iOS'; - this.formFactor='phone'; - this.dyPortrait=this.dyLandscape=25; - } - - if(agent.indexOf('Android') >= 0) { - this.OS='Android'; - this.formFactor='phone'; // form factor may be redefined on initialization - this.dyPortrait=75; - this.dyLandscape=25; - try { - var rx=new RegExp("(?:Android\\s+)(\\d+\\.\\d+\\.\\d+)"); - this.version=agent.match(rx)[1]; - } catch(ex) {} - } - if(agent.indexOf('Windows NT') >= 0) { - this.OS='Windows'; - if(agent.indexOf('Touch') >= 0) { - this.formFactor='phone'; // will be redefined as tablet if resolution high enough - } - - // Windows Phone and Tablet PC - if(typeof navigator.msMaxTouchPoints == 'number' && navigator.msMaxTouchPoints > 0) { - this.touchable=true; - } - } - } - - // var sxx=device.formFactor; - // Check and possibly revise form factor according to actual screen size (adjusted for Galaxy S, maybe OK generally?) - if(this.formFactor == 'tablet' && Math.min(screen.width,screen.height) < 400) { - this.formFactor='phone'; - } - - if(this.formFactor == 'phone' && Math.max(screen.width,screen.height) > 720) { - this.formFactor='tablet'; - } - - // alert(sxx+'->'+device.formFactor); - // Check for phony iOS devices (Win32 test excludes Chrome touch emulation on Windows)! - if(this.OS == 'iOS' && !('ongesturestart' in window) && navigator.platform != 'Win32') { - this.OS='Android'; - } - - // Determine application or browser - this.browser='web'; - if(IEVersion < 999) { - this.browser='ie'; - } else { - if(this.OS == 'iOS' || this.OS.toLowerCase() == 'macosx') { - this.browser='safari'; - } - - var bMatch=/Firefox|Chrome|OPR/; - if(bMatch.test(navigator.userAgent)) { - if((navigator.userAgent.indexOf('Firefox') >= 0) && ('onmozorientationchange' in screen)) { - this.browser='firefox'; - } else if(navigator.userAgent.indexOf('OPR') >= 0) { - this.browser='opera'; - } else if(navigator.userAgent.indexOf('Chrome') >= 0) { - this.browser='chrome'; - } - } - } - } -} - -// The master Util object. --------------------------------------------------------------------------- +// Includes the Device definition set. +/// class Util { // Generalized component event registration @@ -156,7 +33,7 @@ class Util { this.activeDevice = this.device; // Initialize the true device values. - this.device.detect(this._GetIEVersion()); + this.device.detect(); /* DEBUG: Force touch device (Build 360) @@ -458,48 +335,10 @@ class Util { * @return {number} * Description Return IE version number (or 999 if browser not IE) */ - _GetIEVersion() { - var n, agent=''; - - if('userAgent' in navigator) { - agent=navigator.userAgent; - } - - // Test first for old versions - if('selection' in document) { // only defined for IE and not for IE 11!!! - var appVer=navigator.appVersion; - n=appVer.indexOf('MSIE '); - if(n >= 0) { - // Check for quirks mode page, always return 6 if so - if(document.compatMode == 'BackCompat') { - return 6; - } - - appVer=appVer.substr(n+5); - n=appVer.indexOf('.'); - if(n > 0) { - return parseInt(appVer.substr(0,n),10); - } - } - } - - // Finally test for IE 11 (and later?) - n=agent.indexOf('Trident/'); - if(n < 0) { - return 999; - } - - agent=agent.substr(n+8); - n=agent.indexOf('.'); - if(n > 0){ - return parseInt(agent.substr(0,n),10)+4; - } - - return 999; + getIEVersion() { + Device._GetIEVersion(); } - getIEVersion = this._GetIEVersion; - /** * Get browser-independent computed style value for element * @@ -817,7 +656,7 @@ class Util { +fd['family']+';\nfont-style:normal;\nfont-weight:normal;\n'; // Detect if Internet Explorer and version if so - var IE=this._GetIEVersion(); + var IE=Device._GetIEVersion(); // Build the font source string according to the browser, // but return without adding the style sheet if the required font type is unavailable diff --git a/web/source/recorder_InputEvents.ts b/web/source/recorder_InputEvents.ts index c2f9671c67e..d1a34bd2c7d 100644 --- a/web/source/recorder_InputEvents.ts +++ b/web/source/recorder_InputEvents.ts @@ -1,4 +1,10 @@ +// Includes KeymanWeb's Device class, as it's quite useful for ensuring that we target our tests correctly +// to each device. +/// + namespace KMWRecorder { + type AssertCallback = (s1: any, s2: any, msg?: string) => void; + export abstract class InputEvent { abstract simulateEventOn(ele: HTMLElement): void; @@ -232,7 +238,7 @@ namespace KMWRecorder { this.output = output; } - simulateSequenceOn(ele: HTMLElement, assertCallback: (s1: any, s2: any, msg?: string) => void): boolean { + simulateSequenceOn(ele: HTMLElement, assertCallback?: AssertCallback): boolean { resetElement(ele); for(var i=0; i < this.inputs.length; i++) { @@ -256,7 +262,6 @@ namespace KMWRecorder { toPrettyJSON(): string { var str = "{ \"output\": \"" + this.output + "\", \"inputs\": [\n"; for(var i = 0; i < this.inputs.length; i++) { - console.log(this.inputs[i].toPrettyJSON()); str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); } str += "]}"; @@ -323,4 +328,187 @@ namespace KMWRecorder { this.languages = [new LanguageStubForKeyboard(activeStub)]; } } + + type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; + type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; + type BROWSER = 'ie'|'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! + + class Constraint { + target: TARGET; + validOSList?: OS[]; + validBrowsers?: BROWSER[]; + + constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { + if(typeof(target) == 'string') { + this.target = target; + this.validOSList = validOSList; + this.validBrowsers = validBrowsers; + } else { + var json = target; + this.target = json.target; + this.validOSList = json.validOSList; + this.validBrowsers = json.validBrowsers; + } + } + + matchesClient(device: Device, usingOSK?: boolean) { + // #1: Platform check. + if(usingOSK === true) { + if(this.target != device.formFactor) { + return false; + } + } else if(usingOSK === false) { + if(this.target != 'hardware') { + return false; + } + } else if(this.target != device.formFactor && this.target != 'hardware') { + return false; + } + + if(this.validOSList) { + if(this.validOSList.indexOf(device.OS as OS) == -1) { + return false; + } + } + + if(this.validBrowsers) { + if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { + return false; + } + } + + return true; + } + + // Checks if another Constraint instance is functionally identical to this one. + equals(other: Constraint) { + if(this.target != other.target) { + return false; + } + + var list1 = this.validOSList ? this.validOSList : ['any']; + var list2 = other.validOSList ? other.validOSList : ['any']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + list1 = this.validBrowsers ? this.validBrowsers : ['web']; + list2 = other.validBrowsers ? other.validBrowsers : ['web']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + return true; + } + } + + class InputTestSet { + constraint: Constraint; + testSet: InputTestSequence[]; + + constructor(constraint: Constraint|InputTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as InputTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new InputTestSequence(json.testSet[i])); + } + } + } + + addTest(seq: InputTestSequence) { + this.testSet.push(seq); + } + + // Validity should be checked before calling this method. + run(ele: HTMLElement, assertCallback?: AssertCallback) { + for(var i=0; i < this.testSet.length; i++) { + var testSeq = this.testSet[i]; + if(!testSeq.simulateSequenceOn(ele, assertCallback)) { + // Failed test! + // TODO: Make a list of failures and store so that we can mass-report errors! + console.error("Failed test. :("); + } + } + } + + // Used to determine if the current InputTestSet is applicable to be run on a device. + isValidForCurrentClient(usingOSK?: boolean) { + var device: Device = new Device(); + device.detect(); + + return this.constraint.matchesClient(device, usingOSK); + } + } + + class KeyboardTest { + /** + * The stub information to be passed into keyman.addKeyboards() in order to run the test. + */ + keyboard: KeyboardStub; + + /** + * The master array of test sets, each of which specifies constraints a client must fulfill for + * the tests contained therein to be valid. + */ + inputTestSets: InputTestSet[]; + + /** + * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. + * @param fromJSON + */ + constructor(fromJSON: any) { + if(typeof(fromJSON) == 'string') { + fromJSON = JSON.parse(fromJSON); + } + + this.keyboard = new KeyboardStub(fromJSON.keyboard); + this.inputTestSets = []; + + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new InputTestSet(fromJSON.inputTestSets[i]); + } + } + + addTest(constraint: Constraint, seq: InputTestSequence) { + for(var i=0; i < this.inputTestSets.length; i++) { + if(this.inputTestSets[i].constraint.equals(constraint)) { + this.inputTestSets[i].addTest(seq); + return; + } + } + + var newSet = new InputTestSet(new Constraint(constraint)); + this.inputTestSets.push(newSet); + newSet.addTest(seq); + } + + run(ele: HTMLElement, assertCallback?: AssertCallback) { + var setHasRun = false; + + for(var i = 0; i < this.inputTestSets.length; i++) { + var testSet = this.inputTestSets[i]; + + if(testSet.isValidForCurrentClient()) { + // TODO: Collect a set of failure results to report back upon at the end! + testSet.run(ele, assertCallback); + setHasRun = true; + } + } + + // TODO: Report back on the failures! + if(!setHasRun) { + // The sets CAN be empty, allowing silent failure if/when we actually want that. + console.warn("No test sets for this keyboard were applicable for this device!"); + } + } + } } \ No newline at end of file From 5ec722efadcb8f3e4c05e26c5c01b9ee937b79ba Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 13 Feb 2018 21:25:55 +0700 Subject: [PATCH 19/26] Added initial constructor for the KeyboardTest class. --- web/source/recorder_InputEvents.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/source/recorder_InputEvents.ts b/web/source/recorder_InputEvents.ts index d1a34bd2c7d..2f15bbd22fd 100644 --- a/web/source/recorder_InputEvents.ts +++ b/web/source/recorder_InputEvents.ts @@ -465,11 +465,17 @@ namespace KMWRecorder { * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. * @param fromJSON */ - constructor(fromJSON: any) { + constructor(fromJSON: string|KeyboardStub|KeyboardTest) { if(typeof(fromJSON) == 'string') { fromJSON = JSON.parse(fromJSON); + } else if(fromJSON instanceof KeyboardStub) { + this.keyboard = fromJSON; + this.inputTestSets = []; + return; } + fromJSON = fromJSON as KeyboardTest; + this.keyboard = new KeyboardStub(fromJSON.keyboard); this.inputTestSets = []; From 7ebb7307e73476f3d9144858d33ac49ba6c34685 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 14 Feb 2018 13:28:45 +0700 Subject: [PATCH 20/26] Recorder infrastructure and UI now only need polish. --- web/source/kmwRecorder.html | 300 ++++++++++--------------- web/source/recorder_InputEvents.ts | 121 +++++++--- web/source/recorder_KeyboardScripts.js | 6 +- web/source/recorder_ui_and_stubs.js | 288 ++++++++++++++++++++++++ 4 files changed, 502 insertions(+), 213 deletions(-) create mode 100644 web/source/recorder_ui_and_stubs.js diff --git a/web/source/kmwRecorder.html b/web/source/kmwRecorder.html index 68b47792e96..ce3b3d66c3a 100644 --- a/web/source/kmwRecorder.html +++ b/web/source/kmwRecorder.html @@ -39,209 +39,145 @@ width: 50%; min-width: 600px; } + + td { + vertical-align:top; + padding:15px; + } - - - - - + - - -

KeymanWeb - Test Input Recorder

-

This page is designed to record KeymanWeb input for use in test case development.

+ + + + +
+

KeymanWeb - Test Input Recorder

+

This page is designed to record KeymanWeb input for use in test case development.

+

-
-
-

Active device information:

-

Recorded output JSON:

- -

You must click 'Set Sequence' to save any changes to an input sequence above before continuing input recording, or they will be lost upon new input.

- - - -
-
-
-

Record input here:

- -
-
-
-

Currently active stub (for use with addKeyboards):

- -
-
+ + + + +
+

Load Existing JSON

+ +
+
+ + + + + +
+

Load a Keyboard

+

Add a keyboard by keyboard stub (to addKeyboards):

+
+ +
+

Add a keyboard by keyboard name:

+ + + +

Add a keyboard by ISO 639 or BCP-47 language code:

+ + + +

Add a keyboard by language name:

+ + +

- - -

Add a keyboard by keyboard stub (to addKeyboards):

-
- - -

Add a keyboard by keyboard name:

- - - -

Add a keyboard by ISO 639 language code:

- - - -

Add a keyboard by language name:

- - -

- Return to testing home page -

+ + + + + +
+

Keyboard Selection

+

Each defined test set should utilize the same keyboard.

+

Select a keyboard:

+

Please ensure a copy of this keyboard's *.js resides at the specified location within the unit_tests/ folder.

+
+

Currently active stub (for use with addKeyboards):

+ +
+
+ + + + + +
+

Constraint Selection

+

Active form factor:

+

Touch availablility:

+

Current OS:

+

Current browser:

+
+

Valid OS:

+ Any + Windows + Mac OS X + Linux + Android + iOS +

Valid browser:

+ Any + Chrome + Firefox + Safari + IE + Opera +
+
+ + + + + +
+

Input Recording

+

Record input here:

+ +

You must click 'Set Sequence' to save any changes to an input sequence above + before continuing input recording, or they will be lost upon new input.

+
+

Current test sequence JSON:

+ + + + + + +
+
+ + + + +
+

Test suite JSON:

+ +
\ No newline at end of file diff --git a/web/source/recorder_InputEvents.ts b/web/source/recorder_InputEvents.ts index 2f15bbd22fd..cb1d7944173 100644 --- a/web/source/recorder_InputEvents.ts +++ b/web/source/recorder_InputEvents.ts @@ -238,7 +238,7 @@ namespace KMWRecorder { this.output = output; } - simulateSequenceOn(ele: HTMLElement, assertCallback?: AssertCallback): boolean { + simulateSequenceOn(ele: HTMLElement, assertCallback?: AssertCallback): {success: boolean, result: string} { resetElement(ele); for(var i=0; i < this.inputs.length; i++) { @@ -256,7 +256,7 @@ namespace KMWRecorder { assertCallback(result, this.output, this.msg); } - return result == this.output; + return {success: (result == this.output), result: result}; } toPrettyJSON(): string { @@ -267,6 +267,16 @@ namespace KMWRecorder { str += "]}"; return str; } + + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof OSKInputEvent) { + return true; + } + } + + return false; + } } class FontStubForLanguage { @@ -296,16 +306,26 @@ namespace KMWRecorder { oskFont?: FontStubForLanguage; constructor(activeStub: any) { - this.id = activeStub.KLC; - this.name = activeStub.KL; - this.region = activeStub.KR; + if(activeStub.KLC) { + this.id = activeStub.KLC; + this.name = activeStub.KL; + this.region = activeStub.KR; + + // Fonts. + if(activeStub.KFont) { + this.font = new FontStubForLanguage(activeStub.KFont); + } + if(activeStub.KOskFont) { + this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + } + } else { + this.id = activeStub.id; + this.name = activeStub.name; + this.region = activeStub.region; - // Fonts. - if(activeStub.KFont) { - this.font = new FontStubForLanguage(activeStub.KFont); - } - if(activeStub.KOskFont) { - this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. + this.font = activeStub.font; + this.oskFont = activeStub.oskFont; } } } @@ -319,13 +339,29 @@ namespace KMWRecorder { // Constructs a stub usable with KeymanWeb's addKeyboards() API function from // the internally-tracked ActiveStub value for that keyboard. constructor(activeStub: any) { - this.id = activeStub.KI; - this.id = this.id.replace('Keyboard_', ''); + if(activeStub.KI) { + this.id = activeStub.KI; + this.id = this.id.replace('Keyboard_', ''); + + this.name = activeStub.KN; + this.filename = activeStub.KF; + + this.languages = [new LanguageStubForKeyboard(activeStub)]; + } else { + this.id = activeStub.id; + this.name = activeStub.name; + this.filename = activeStub.filename; + this.languages = [] - this.name = activeStub.KN; - this.filename = activeStub.KF; + for(var i=0; i < activeStub.languages.length; i++) { + this.languages.push(new LanguageStubForKeyboard(activeStub.languages[i])); + } + } + } - this.languages = [new LanguageStubForKeyboard(activeStub)]; + setBasePath(filePath: string) { + var file = this.filename.substr(this.filename.lastIndexOf('/')+1); + this.filename = filePath + '/' + file; } } @@ -333,7 +369,7 @@ namespace KMWRecorder { type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; type BROWSER = 'ie'|'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! - class Constraint { + export class Constraint { target: TARGET; validOSList?: OS[]; validBrowsers?: BROWSER[]; @@ -404,6 +440,18 @@ namespace KMWRecorder { } } + class TestFailure { + constraint: Constraint; + test: InputTestSequence; + result: string; + + constructor(constraint: Constraint, test: InputTestSequence, output: string) { + this.constraint = constraint; + this.test = test; + this.result = output; + } + } + class InputTestSet { constraint: Constraint; testSet: InputTestSequence[]; @@ -429,15 +477,19 @@ namespace KMWRecorder { } // Validity should be checked before calling this method. - run(ele: HTMLElement, assertCallback?: AssertCallback) { + run(ele: HTMLElement, assertCallback?: AssertCallback): TestFailure[] { + var failures: TestFailure[] = []; + for(var i=0; i < this.testSet.length; i++) { var testSeq = this.testSet[i]; - if(!testSeq.simulateSequenceOn(ele, assertCallback)) { + var simResult = testSeq.simulateSequenceOn(ele, assertCallback); + if(!simResult.success) { // Failed test! - // TODO: Make a list of failures and store so that we can mass-report errors! - console.error("Failed test. :("); + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); } } + + return failures.length > 0 ? failures : null; } // Used to determine if the current InputTestSet is applicable to be run on a device. @@ -449,7 +501,7 @@ namespace KMWRecorder { } } - class KeyboardTest { + export class KeyboardTest { /** * The stub information to be passed into keyman.addKeyboards() in order to run the test. */ @@ -466,7 +518,11 @@ namespace KMWRecorder { * @param fromJSON */ constructor(fromJSON: string|KeyboardStub|KeyboardTest) { - if(typeof(fromJSON) == 'string') { + if(!fromJSON) { + this.keyboard = null; + this.inputTestSets = []; + return; + } else if(typeof(fromJSON) == 'string') { fromJSON = JSON.parse(fromJSON); } else if(fromJSON instanceof KeyboardStub) { this.keyboard = fromJSON; @@ -499,22 +555,35 @@ namespace KMWRecorder { run(ele: HTMLElement, assertCallback?: AssertCallback) { var setHasRun = false; + var failures: TestFailure[] = []; for(var i = 0; i < this.inputTestSets.length; i++) { var testSet = this.inputTestSets[i]; if(testSet.isValidForCurrentClient()) { - // TODO: Collect a set of failure results to report back upon at the end! - testSet.run(ele, assertCallback); + var testFailures = testSet.run(ele, assertCallback); + if(testFailures) { + failures = failures.concat(testFailures); + } setHasRun = true; } } - // TODO: Report back on the failures! if(!setHasRun) { // The sets CAN be empty, allowing silent failure if/when we actually want that. console.warn("No test sets for this keyboard were applicable for this device!"); } + + // Allow the method's caller to trigger a 'fail'. + if(failures.length > 0) { + return failures; + } else { + return null; + } + } + + isEmpty() { + return this.inputTestSets.length == 0; } } } \ No newline at end of file diff --git a/web/source/recorder_KeyboardScripts.js b/web/source/recorder_KeyboardScripts.js index 3c7fd276106..685d08bbe8d 100644 --- a/web/source/recorder_KeyboardScripts.js +++ b/web/source/recorder_KeyboardScripts.js @@ -92,11 +92,7 @@ break; case 2: sKbd=document.getElementById('kbd_id2').value.toLowerCase(); - var rx=new RegExp(/^\w{3,3}$\$?/); - if(rx.test(sKbd)) - kmw.addKeyboards('@'+sKbd); - else - alert('An ISO 639 language code must be exactly 3 letters long!'); + kmw.addKeyboards('@'+sKbd); break; case 3: sKbd=document.getElementById('kbd_id3').value; diff --git a/web/source/recorder_ui_and_stubs.js b/web/source/recorder_ui_and_stubs.js new file mode 100644 index 00000000000..3ed8132a891 --- /dev/null +++ b/web/source/recorder_ui_and_stubs.js @@ -0,0 +1,288 @@ +var UNIT_TEST_FOLDER_RELATIVE_PATH = "../unit_tests"; +var inputJSON = new KMWRecorder.InputTestSequence(); +var testDefinition = new KMWRecorder.KeyboardTest(); + +var ta_inputJSON; +var in_output; + +var justActivated = false; + +function focusReceiver() { + var receiver = document.getElementById('receiver'); + receiver.focus(); +} + +setElementText = function(ele, text) { + ele.value = text; + if(ele['kmw_ip']) { + keyman.touchAliasing.setTextBeforeCaret(ele['kmw_ip'], ele.value); + } +} + +addInputRecord = function(json) { + inputJSON.addInput(json, in_output.value); + setElementText(ta_inputJSON, inputJSON.toPrettyJSON()); +} + +resetInputRecord = function() { + setElementText(ta_inputJSON, ""); + setElementText(in_output, ""); + + inputJSON = new KMWRecorder.InputTestSequence(); +} + +copyInputRecord = function() { + try { + if(!ta_inputJSON['kmw_ip']) { + ta_inputJSON.select(); + } else { + var range = document.createRange(); + range.selectNode(ta_inputJSON['kmw_ip']); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + } + + var res = document.execCommand('copy'); + if(res) { + in_output.focus(); + return; + } + } catch (err) { console.log(err) } + alert("Unable to copy successfully."); +} + +function saveInputRecord() { + var target; + if(inputJSON.hasOSKInteraction()) { + var device = new Device(); + device.detect(); + target = device.formFactor; + } else { + target = 'hardware'; + } + var os_list = getPlatforms(); + var browsers = getBrowsers(); + var config = new KMWRecorder.Constraint(target, os_list, browsers); + + testDefinition.addTest(config, inputJSON); + resetInputRecord(); + setTestDefinition(testDefinition); +} + +reviseInputRecord = function() { + inputJSON = new KMWRecorder.InputTestSequence(JSON.parse(ta_inputJSON.value)); + setElementText(in_output, inputJSON.output) +} + +setTestDefinition = function(testDef) { + if(testDef) { + testDefinition = testDef; + } + var masterJSON = document.getElementById('masterJSON'); + masterJSON.value = JSON.stringify(testDefinition, null, ' '); +} + +// Time for the 'magic'. Yay, JavaScript method extension strategies... +var _kd = keyman.touchAliasing._KeyDown.bind(keyman.touchAliasing); +keyman.touchAliasing._KeyDown = function(e) { + if(DOMEventHandlers.states.activeElement != in_output && + DOMEventHandlers.states.activeElement != in_output['kmw_ip']) { + return _kd(e); + } + + var event = new KMWRecorder.PhysicalInputEvent(e); + var retVal = _kd(e); + + // Record the keystroke as part of a test sequence! + // Miniature delay in case the keyboard relies upon default backspace/delete behavior! + window.setTimeout(function() { + addInputRecord(event); + }, 1); + + return retVal; +} + +var _ock = keyman.osk.clickKey.bind(keyman.osk); +keyman.osk.clickKey = function(e) { + if(DOMEventHandlers.states.activeElement != in_output && + DOMEventHandlers.states.activeElement != in_output['kmw_ip']) { + return _ock(e); + } + + var event = new KMWRecorder.OSKInputEvent(e); + var retVal = _ock(e); + + // Record the click/touch as part of a test sequence! + addInputRecord(event); + return retVal; +} + +var _sak = keyman.keyboardManager._SetActiveKeyboard.bind(keyman.keyboardManager); +keyman.keyboardManager._SetActiveKeyboard = function(PInternalName, PLgCode, saveCookie) { + // If it's not on our recording control, ignore the change and do nothing special. + if(document.activeElement != in_output) { + _sak(PInternalName, PLgCode, saveCookie); + } + + var sameKbd = (testDefinition.keyboard && ("Keyboard_" + testDefinition.keyboard.id) == PInternalName) + && (testDefinition.keyboard.languages[0].id == PLgCode); + + if(!testDefinition.isEmpty() && !sameKbd && !justActivated) { + if(!confirm("Changing the keyboard will clear the current test set. Are you sure?")) { + _sak("Keyboard_" + testDefinition.keyboard.id, testDefinition.keyboard.languages[0].id); + return; + } + } + _sak(PInternalName, PLgCode, saveCookie); + + // What's the active stub immediately after our _SetActiveKeyboard call? + var internalStub = keyman.keyboardManager.activeStub; + if(internalStub && (DOMEventHandlers.states.activeElement == in_output + || DOMEventHandlers.states.activeElement == in_output['kmw_ip'])) { + var kbdRecord = new KMWRecorder.KeyboardStub(internalStub); + kbdRecord.setBasePath('resources/keyboards'); + var ta_activeStub = document.getElementById('activeStub'); + ta_activeStub.value = JSON.stringify(kbdRecord); + + if(!sameKbd && !justActivated) { + setTestDefinition(new KMWRecorder.KeyboardTest(kbdRecord)); + } + } + justActivated = false; +} + +var initDevice = function() { + // From KMW. + var device = new Device(); + device.detect(); + + document.getElementById("activeFormFactor").textContent = device.formFactor; + document.getElementById("activeTouch").textContent = device.touchable ? 'Supported' : 'None'; + document.getElementById("activeOS").textContent = device.OS; + document.getElementById("activeBrowser").textContent = device.browser; +} + +window.addEventListener('load', function() { + ta_inputJSON = document.getElementById('inputRecord'); + in_output = document.getElementById('receiver'); + + keyman.attachToControl(in_output); + keyman.setKeyboardForControl(in_output, '', ''); + resetInputRecord(); + initDevice(); + setupKeyboardPicker(); + setTestDefinition(); +}); + +//var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode}; +function keyboardAdded(properties) { + var kbdControl = document.getElementById('KMW_Keyboard'); + + var opt = document.createElement('OPTION'); + opt.value = properties.internalName + "$$" + properties.languageCode; + opt.innerHTML = properties.keyboardName + " (" + properties.language + ")"; + kbdControl.appendChild(opt); +} + +function setupKeyboardPicker() { + /* Make sure that Keyman is initialized (we can't guarantee initialization order) */ + keyman.init(); + + var kbdControl = document.getElementById('KMW_Keyboard'); + /* Retrieve the list of keyboards available from KeymanWeb and populate the selector using the DOM */ + var kbds = keyman.getKeyboards(); + for(var kbd in kbds) { + var opt = document.createElement('OPTION'); + opt.value = kbds[kbd].InternalName + "$$" + kbds[kbd].LanguageCode; + opt.innerHTML = kbds[kbd].Name; + kbdControl.appendChild(opt); + } + + // Ensures the default keyboard is active, to match our listbox's initial (default) option. + keyman.setActiveKeyboard('', ''); + keyman.addEventListener('keyboardregistered', keyboardAdded); +} + +/* Called when user selects an item in the KMW_Keyboard SELECT */ +function KMW_KeyboardChange() { + var kbdControl = document.getElementById('KMW_Keyboard'); + /* Select the keyboard in KeymanWeb */ + var name = kbdControl.value.substr(0, kbdControl.value.indexOf("$$")); + var languageCode = kbdControl.value.substr(kbdControl.value.indexOf("$$")+2); + + var activeElement = document.activeElement; + justActivated = true; + focusReceiver(); + kmw.setActiveKeyboard(name, languageCode); + activeElement.focus(); +} + +function loadExistingTest(files) { + if(files.length > 0) { + var reader = new FileReader(); + reader.onload = function() { + try { + var kbdTest = new KMWRecorder.KeyboardTest(reader.result); + setTestDefinition(kbdTest) + + // Make sure we've loaded the keyboard! Problem - we're not running from the unit_tests folder! + var kbdStub = new KMWRecorder.KeyboardStub(kbdTest.keyboard); + kbdStub.filename = UNIT_TEST_FOLDER_RELATIVE_PATH + "/" + kbdStub.filename; + + keyman.addKeyboards(kbdStub); + + var activeElement = document.activeElement; + justActivated = true; + focusReceiver(); + keyman.setActiveKeyboard("Keyboard_" + kbdTest.keyboard.id, kbdTest.keyboard.languages[0].id); + activeElement.focus(); + } catch (e) { + alert("File does not contain a valid KeyboardTest definition.") + console.error(e); + } + } + reader.readAsText(files[0]); + } +} + +var PLATFORMS = ['windows', 'macosx', 'linux', 'android', 'ios']; +var BROWSERS = ['ie', 'chrome', 'firefox', 'safari', 'opera']; + +function _clearCategory(arr, prefix) { + for(var i=0; i < arr.length; i++) { + var cb = document.getElementById(prefix + arr[i]); + cb.checked = false; + } + + _setCategoryAny(arr, prefix); +} + +function _setCategoryAny(arr, prefix) { + var any = true; + for(var i=0; i < arr.length; i++) { + var cb = document.getElementById(prefix + arr[i]); + if(cb.checked) { + any = false; + } + } + document.getElementById(prefix + 'any').checked = any; +} + +function _getCategory(arr, prefix) { + var res = []; + for(var i=0; i < arr.length; i++) { + var cb = document.getElementById(prefix + arr[i]); + if(cb.checked) { + res.push(arr[i]); + } + } + + return res.length > 0 ? res : null; +} + +function clearPlatforms() { _clearCategory( PLATFORMS, "platform_"); } +function setPlatformAny() { _setCategoryAny( PLATFORMS, "platform_"); } +function getPlatforms() { return _getCategory(PLATFORMS, "platform_"); } +function clearBrowsers() { _clearCategory( BROWSERS, "browser_"); } +function setBrowserAny() { _setCategoryAny( BROWSERS, "browser_"); } +function getBrowsers() { return _getCategory(BROWSERS, "browser_"); } \ No newline at end of file From 428ba42802f2382304d111b745ff1658bf37a349 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 14 Feb 2018 14:05:19 +0700 Subject: [PATCH 21/26] The new OO structure is now used by the example engine test case. --- web/source/recorder_InputEvents.ts | 6 +- web/unit_tests/cases/engine.js | 42 ++------- .../engine_tests/basic_lao_simulation.json | 92 +++++++++++++++++++ web/unit_tests/test_utils.js | 29 +++++- 4 files changed, 128 insertions(+), 41 deletions(-) create mode 100644 web/unit_tests/json/engine_tests/basic_lao_simulation.json diff --git a/web/source/recorder_InputEvents.ts b/web/source/recorder_InputEvents.ts index cb1d7944173..3bb98aa31f2 100644 --- a/web/source/recorder_InputEvents.ts +++ b/web/source/recorder_InputEvents.ts @@ -274,7 +274,7 @@ namespace KMWRecorder { return true; } } - + return false; } } @@ -553,14 +553,14 @@ namespace KMWRecorder { newSet.addTest(seq); } - run(ele: HTMLElement, assertCallback?: AssertCallback) { + run(ele: HTMLElement, usingOSK?: boolean, assertCallback?: AssertCallback) { var setHasRun = false; var failures: TestFailure[] = []; for(var i = 0; i < this.inputTestSets.length; i++) { var testSet = this.inputTestSets[i]; - if(testSet.isValidForCurrentClient()) { + if(testSet.isValidForCurrentClient(usingOSK)) { var testFailures = testSet.run(ele, assertCallback); if(testFailures) { failures = failures.concat(testFailures); diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index b9aa59895dd..dc0ef1a5f67 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -95,42 +95,14 @@ describe('Engine', function() { }) describe('Sequence Testing', function() { - before(function(done){ - this.timeout = 10000; - loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", done, 10000); - }); - - after(function() { - keyman.removeKeyboards('lao_2008_basic'); - fixture.cleanup(); - }); - - it('Keyboard simulation', function() { - var inputElem = document.getElementById('singleton'); - if(inputElem['kmw_ip']) { - inputElem = inputElem['kmw_ip']; - } + this.timeout(10000); - var lao_s_key_json = { - "inputs": [{"type":"key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0}], - "output": "ຫ" - }; - var lao_s_test = new KMWRecorder.InputTestSequence(lao_s_key_json); - lao_s_test.simulateSequenceOn(inputElem, assert.equal); + it('Keyboard simulation', function(done) { + runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', false, done, assert.equal, 10000); }); - it('OSK simulation', function() { - var inputElem = document.getElementById('singleton'); - if(inputElem['kmw_ip']) { - inputElem = inputElem['kmw_ip']; - } - - var lao_s_osk_json = { - "inputs": [{"type":"osk", "keyID": 'shift-K_S'}], - "output": ";" - }; - var lao_s_test = new KMWRecorder.InputTestSequence(lao_s_osk_json); - lao_s_test.simulateSequenceOn(inputElem, assert.equal); - }); - }) + it('OSK simulation', function(done) { + runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', true, done, assert.equal, 10000); + }) + }); }); \ No newline at end of file diff --git a/web/unit_tests/json/engine_tests/basic_lao_simulation.json b/web/unit_tests/json/engine_tests/basic_lao_simulation.json new file mode 100644 index 00000000000..8a5dcf0897c --- /dev/null +++ b/web/unit_tests/json/engine_tests/basic_lao_simulation.json @@ -0,0 +1,92 @@ +{ + "keyboard": { + "id": "lao_2008_basic", + "name": "Lao Basic", + "filename": "resources/keyboards/lao_2008_basic.js", + "languages": [ + { + "id": "lao", + "name": "Lao", + "region": "Asia" + } + ] + }, + "inputTestSets": [ + { + "constraint": { + "target": "hardware", + "validOSList": null, + "validBrowsers": null + }, + "testSet": [ + { + "inputs": [ + { + "type": "key", + "key": "s", + "code": "KeyS", + "keyCode": 83, + "modifierSet": 0, + "location": 0 + } + ], + "output": "ຫ" + } + ] + }, + { + "constraint": { + "target": "desktop", + "validOSList": null, + "validBrowsers": null + }, + "testSet": [ + { + "inputs": [ + { + "type": "osk", + "keyID": "default-K_S" + } + ], + "output": "ຫ" + } + ] + }, + { + "constraint": { + "target": "phone", + "validOSList": null, + "validBrowsers": null + }, + "testSet": [ + { + "inputs": [ + { + "type": "osk", + "keyID": "default-K_S" + } + ], + "output": "ຫ" + } + ] + }, + { + "constraint": { + "target": "tablet", + "validOSList": null, + "validBrowsers": null + }, + "testSet": [ + { + "inputs": [ + { + "type": "osk", + "keyID": "default-K_S" + } + ], + "output": "ຫ" + } + ] + } + ] +} \ No newline at end of file diff --git a/web/unit_tests/test_utils.js b/web/unit_tests/test_utils.js index 01fae561b7d..a1d4a9c6cad 100644 --- a/web/unit_tests/test_utils.js +++ b/web/unit_tests/test_utils.js @@ -160,9 +160,7 @@ var onScriptLoad = function(scriptURL, callback, timeout) { slo.observe(); }; -var loadKeyboardFromJSON = function(jsonPath, callback, timeout) { - var stub = fixture.load(jsonPath, true); - +var loadKeyboardStub = function(stub, callback, timeout) { var kbdName = "Keyboard_" + stub.id; keyman.addKeyboards(stub); @@ -175,4 +173,29 @@ var loadKeyboardFromJSON = function(jsonPath, callback, timeout) { } else { callback(); } +} + +var loadKeyboardFromJSON = function(jsonPath, callback, timeout) { + var stub = fixture.load(jsonPath, true); + + loadKeyboardStub(stub, callback, timeout); +} + +function runLoadedKeyboardTest(testDef, usingOSK, assertCallback) { + var inputElem = document.getElementById('singleton'); + if(inputElem['kmw_ip']) { + inputElem = inputElem['kmw_ip']; + } + + testDef.run(inputElem, usingOSK, assertCallback); +} + +function runKeyboardTestFromJSON(jsonPath, usingOSK, callback, assertCallback, timeout) { + var testSpec = new KMWRecorder.KeyboardTest(fixture.load(jsonPath, true)); + + loadKeyboardStub(testSpec.keyboard, function() { + runLoadedKeyboardTest(testSpec, usingOSK, assertCallback); + keyman.removeKeyboards(testSpec.keyboard.id); + callback(); + }, timeout); } \ No newline at end of file From 819e37a58bb5d6bd818800518f0d074b2492d294 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 14 Feb 2018 14:19:55 +0700 Subject: [PATCH 22/26] Minor test runner parameter tweak for readability. --- web/unit_tests/cases/engine.js | 4 ++-- web/unit_tests/test_utils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/unit_tests/cases/engine.js b/web/unit_tests/cases/engine.js index dc0ef1a5f67..955a846bb98 100644 --- a/web/unit_tests/cases/engine.js +++ b/web/unit_tests/cases/engine.js @@ -98,11 +98,11 @@ describe('Engine', function() { this.timeout(10000); it('Keyboard simulation', function(done) { - runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', false, done, assert.equal, 10000); + runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: false}, done, assert.equal, 10000); }); it('OSK simulation', function(done) { - runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', true, done, assert.equal, 10000); + runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: true}, done, assert.equal, 10000); }) }); }); \ No newline at end of file diff --git a/web/unit_tests/test_utils.js b/web/unit_tests/test_utils.js index a1d4a9c6cad..6bd17008e84 100644 --- a/web/unit_tests/test_utils.js +++ b/web/unit_tests/test_utils.js @@ -190,11 +190,11 @@ function runLoadedKeyboardTest(testDef, usingOSK, assertCallback) { testDef.run(inputElem, usingOSK, assertCallback); } -function runKeyboardTestFromJSON(jsonPath, usingOSK, callback, assertCallback, timeout) { +function runKeyboardTestFromJSON(jsonPath, params, callback, assertCallback, timeout) { var testSpec = new KMWRecorder.KeyboardTest(fixture.load(jsonPath, true)); loadKeyboardStub(testSpec.keyboard, function() { - runLoadedKeyboardTest(testSpec, usingOSK, assertCallback); + runLoadedKeyboardTest(testSpec, params.usingOSK, assertCallback); keyman.removeKeyboards(testSpec.keyboard.id); callback(); }, timeout); From 0f1414926ba03e606a567042165b3cab86433c50 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 14 Feb 2018 15:28:32 +0700 Subject: [PATCH 23/26] Tweaks recorder touch support to better load existing JSON. --- web/source/recorder_ui_and_stubs.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/source/recorder_ui_and_stubs.js b/web/source/recorder_ui_and_stubs.js index 3ed8132a891..73fcb2efd9b 100644 --- a/web/source/recorder_ui_and_stubs.js +++ b/web/source/recorder_ui_and_stubs.js @@ -9,7 +9,17 @@ var justActivated = false; function focusReceiver() { var receiver = document.getElementById('receiver'); + if(receiver['kmw_ip']) { + receiver = receiver['kmw_ip']; + } receiver.focus(); + + if(keyman.util.device.touchable) { + // At present, touch doesn't 'focus' properly. + DOMEventHandlers.states.lastActiveElement = receiver; + DOMEventHandlers.states.activeElement = receiver; + keyman.osk.show(true); + } } setElementText = function(ele, text) { @@ -172,6 +182,8 @@ window.addEventListener('load', function() { initDevice(); setupKeyboardPicker(); setTestDefinition(); + + DOMEventHandlers.states.lastActiveElement = in_output['kmw_ip'] ? in_output['kmw_ip'] : in_output; }); //var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode}; From 0725a8f19532a3fda96873005013d41b9e6e3b60 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 14 Feb 2018 15:52:49 +0700 Subject: [PATCH 24/26] Now takes in per-sequence error messages if specified. --- web/source/kmwRecorder.html | 2 ++ web/source/recorder_InputEvents.ts | 12 ++++++++++-- web/source/recorder_ui_and_stubs.js | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/web/source/kmwRecorder.html b/web/source/kmwRecorder.html index ce3b3d66c3a..fc694c1a178 100644 --- a/web/source/kmwRecorder.html +++ b/web/source/kmwRecorder.html @@ -153,6 +153,8 @@

Constraint Selection

Input Recording

+

Error message here:

+

Record input here:

You must click 'Set Sequence' to save any changes to an input sequence above diff --git a/web/source/recorder_InputEvents.ts b/web/source/recorder_InputEvents.ts index 3bb98aa31f2..bc2a887d597 100644 --- a/web/source/recorder_InputEvents.ts +++ b/web/source/recorder_InputEvents.ts @@ -260,11 +260,19 @@ namespace KMWRecorder { } toPrettyJSON(): string { - var str = "{ \"output\": \"" + this.output + "\", \"inputs\": [\n"; + var str = "{ "; + if(this.output) { + str += "\"output\": \"" + this.output + "\", "; + } + str += "\"inputs\": [\n"; for(var i = 0; i < this.inputs.length; i++) { str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); } - str += "]}"; + if(this.msg) { + str += "], \"message\": \"" + this.msg + "\" }"; + } else { + str += "]}"; + } return str; } diff --git a/web/source/recorder_ui_and_stubs.js b/web/source/recorder_ui_and_stubs.js index 73fcb2efd9b..4e3057658f4 100644 --- a/web/source/recorder_ui_and_stubs.js +++ b/web/source/recorder_ui_and_stubs.js @@ -161,6 +161,17 @@ keyman.keyboardManager._SetActiveKeyboard = function(PInternalName, PLgCode, sav justActivated = false; } +function errorUpdate() { + var errorInput = document.getElementById('errorText'); + if(errorInput.value) { + inputJSON.msg = errorInput.value; + } else { + delete inputJSON.msg; + } + + setElementText(ta_inputJSON, inputJSON.toPrettyJSON()); +} + var initDevice = function() { // From KMW. var device = new Device(); @@ -184,6 +195,18 @@ window.addEventListener('load', function() { setTestDefinition(); DOMEventHandlers.states.lastActiveElement = in_output['kmw_ip'] ? in_output['kmw_ip'] : in_output; + + var errorInput = document.getElementById('errorText'); + if(errorInput['kmw_ip']) { + // Alias DIVs use subelements b/c caret simulation. + // Interestingly, 'childList' is the most important for noting textContent changes. + var config = { childList: true, subtree: true }; + var observer = new MutationObserver(function(mutations) { + errorUpdate(); + }); + + observer.observe(errorInput['kmw_ip'], config); + } }); //var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode}; From ffa76e746831d664083097946c240bdbfe28fb54 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 15 Feb 2018 08:31:34 +0700 Subject: [PATCH 25/26] Fixes for PR comments. --- web/source/build_recorder.sh | 2 +- web/source/kmwRecorder.html | 39 +++++++++++++++++++++++------------- web/source/kmwutils.ts | 2 +- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/web/source/build_recorder.sh b/web/source/build_recorder.sh index c9301961487..ef1cb401a3f 100644 --- a/web/source/build_recorder.sh +++ b/web/source/build_recorder.sh @@ -1,6 +1,6 @@ #! /bin/bash # -# Compile keymanweb and copy compiled javascript and resources to output/embedded folder +# Compile the KeymanWeb Recorder module for use with developing/running engine tests. # # Fails the build if a specified file does not exist. diff --git a/web/source/kmwRecorder.html b/web/source/kmwRecorder.html index fc694c1a178..8572c5536cd 100644 --- a/web/source/kmwRecorder.html +++ b/web/source/kmwRecorder.html @@ -54,8 +54,7 @@ @@ -132,19 +131,31 @@

Constraint Selection

Valid OS:

- Any - Windows - Mac OS X - Linux - Android - iOS + + + + + + + + + + + +

Valid browser:

- Any - Chrome - Firefox - Safari - IE - Opera + + + + + + + + + + + + diff --git a/web/source/kmwutils.ts b/web/source/kmwutils.ts index 3902ae386bd..dba82fbea20 100644 --- a/web/source/kmwutils.ts +++ b/web/source/kmwutils.ts @@ -336,7 +336,7 @@ class Util { * Description Return IE version number (or 999 if browser not IE) */ getIEVersion() { - Device._GetIEVersion(); + return Device._GetIEVersion(); } /** From 049f7a7d57901358a2b234666f832a5a29935572 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 15 Feb 2018 08:49:34 +0700 Subject: [PATCH 26/26] Missed a minor detail and addressed the OSK BKSP key thing. --- web/source/kmwosk.ts | 3 --- web/source/recorder_ui_and_stubs.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/web/source/kmwosk.ts b/web/source/kmwosk.ts index 6d187923912..4e37536842a 100644 --- a/web/source/kmwosk.ts +++ b/web/source/kmwosk.ts @@ -1034,9 +1034,6 @@ if(!window['keyman']['initialized']) { if(keyName == 'K_LOPT' || keyName == 'K_ROPT') { osk.optionKey(e,keyName,true); return true; - } else if(keyName == 'K_BKSP') { - kbdInterface.output(1, keymanweb.domManager.getLastActiveElement(), ""); - return true; } // Turn off key highlighting (or preview) diff --git a/web/source/recorder_ui_and_stubs.js b/web/source/recorder_ui_and_stubs.js index 4e3057658f4..980b3939272 100644 --- a/web/source/recorder_ui_and_stubs.js +++ b/web/source/recorder_ui_and_stubs.js @@ -248,7 +248,7 @@ function KMW_KeyboardChange() { var activeElement = document.activeElement; justActivated = true; focusReceiver(); - kmw.setActiveKeyboard(name, languageCode); + keyman.setActiveKeyboard(name, languageCode); activeElement.focus(); }