Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,437 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var assert = require('chai').assert;
var path = require('path');
var fs = require('fs');
var helper = require('helper');
var AdmZip = require('adm-zip');
var jsdom = require('jsdom').jsdom;
function checkLayoutsJSON(zipPath, expectedArr) {
var zip = new AdmZip(zipPath);
var entry = zip.getEntry('js/settings/layouts.json');
var actualArr = JSON.parse(zip.readAsText(entry));
actualArr.forEach(function(layout) {
assert.isTrue(
!!layout.dictFileSize,
'Expect layout contain to non-zero dictFileSize, id=' + layout.id);
delete layout.dictFileSize;
});
expectedArr.forEach(function(layout) {
delete layout.dictFileSize;
});
assert.deepEqual(actualArr, expectedArr);
}
suite('Keyboard layouts building tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
// default make -- build with selected layouts and dictionaries
test('APP=keyboard make', function(done) {
var cmd = 'APP=keyboard make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var config = JSON.parse(process.env.BUILD_CONFIG);
var layoutIds = ['ar',
'bn-Avro',
'bn-Probhat',
'de',
'de-Neo',
'dz-BT',
'en',
'en-Colemak',
'en-Dvorak',
'en-Neo',
'eo',
'es',
'es-Americas',
'fa',
'fr',
'fr-CA',
'fr-CH',
'fr-Dvorak-bepo',
'he',
'hi',
'ig',
'ko',
'mk',
'my',
'pl',
'pt-BR',
'ta',
'te',
'th',
'uk',
'vi-Qwerty',
'vi-Telex',
'vi-Typewriter',
'wo',
'zh-Hans-Pinyin'];
var zipPath = path.join(process.cwd(), 'profile',
'webapps', 'keyboard.gaiamobile.org', 'application.zip');
var appDirPath = config.GAIA_DIR + '/apps/keyboard';
var layouts = layoutIds.map(function(layout) {
return 'js/layouts/' + layout + '.js';
});
// Unfortunately we can only whilelist files here because we shouldn't
// throw error if excluded files are missing (e.g. README).
var imes = [
'js/imes/jshangul/jshangul.js',
'js/imes/jspinyin/empinyin_files.data',
'js/imes/jspinyin/empinyin_files.js',
'js/imes/jspinyin/jspinyin.js',
'js/imes/jspinyin/libpinyin.js',
'js/imes/jspinyin/worker.js',
'js/imes/latin/latin.js',
'js/imes/latin/predictions.js',
'js/imes/latin/worker.js'
];
var dicts = [
'js/imes/latin/dictionaries/de.dict',
'js/imes/latin/dictionaries/en_us.dict',
'js/imes/latin/dictionaries/es.dict',
'js/imes/latin/dictionaries/fr.dict',
'js/imes/latin/dictionaries/pl.dict',
'js/imes/latin/dictionaries/pt_br.dict'
];
var checkList = [].concat(layouts, imes, dicts);
checkList.forEach(function(path) {
helper.checkFileInZip(zipPath, path, appDirPath + '/' + path);
});
// Verify inputs entry in manifest
var zip = new AdmZip(zipPath);
var entry = zip.getEntry('manifest.webapp');
var manifest = JSON.parse(zip.readAsText(entry));
var inputKeysInManifest = Object.keys(manifest.inputs);
assert.deepEqual(inputKeysInManifest.sort(),
[].concat(layoutIds, 'number', 'emoji').sort());
// Verify dictionary config
var dictJSON = JSON.parse(fs.readFileSync(
appDirPath +
'/test/build/integration/resources/' +
'default-make-layouts.json'));
checkLayoutsJSON(zipPath, dictJSON);
done();
});
});
// Build with all layouts and dictionaries
test('APP=keyboard GAIA_KEYBOARD_LAYOUTS=* make', function(done) {
var cmd = 'APP=keyboard GAIA_KEYBOARD_LAYOUTS=* make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var config = JSON.parse(process.env.BUILD_CONFIG);
var zipPath = path.join(process.cwd(), 'profile',
'webapps', 'keyboard.gaiamobile.org', 'application.zip');
var appDirPath = config.GAIA_DIR + '/apps/keyboard';
var layoutIds =
fs.readdirSync(appDirPath + '/js/layouts').filter(function(filename) {
return (path.extname(filename) === '.js');
}).map(function(filename) {
return path.basename(filename, '.js');
}).filter(function(layoutId) {
// Exclude noIncludeInExpandLayoutIdSet layouts
return (['emoji'].indexOf(layoutId) === -1);
});
var layouts = layoutIds.map(function(layout) {
return 'js/layouts/' + layout + '.js';
});
// Unfortunately we can only whilelist files here because we shouldn't
// throw error if excluded files are missing (e.g. README).
// For the sake of simplicity let's only check the main scripts.
var imes = [
'js/imes/handwriting/handwriting.js',
'js/imes/india/india.js',
'js/imes/jshangul/jshangul.js',
'js/imes/jskanji/jskanji.js',
'js/imes/jspinyin/jspinyin.js',
'js/imes/latin/latin.js',
'js/imes/vietnamese/vietnamese.js',
'js/imes/myanmar/myanmar.js'
];
var dicts = [
'js/imes/latin/dictionaries/bg.dict',
'js/imes/latin/dictionaries/bs.dict',
'js/imes/latin/dictionaries/ca.dict',
'js/imes/latin/dictionaries/cs.dict',
'js/imes/latin/dictionaries/da.dict',
'js/imes/latin/dictionaries/de.dict',
'js/imes/latin/dictionaries/el.dict',
'js/imes/latin/dictionaries/en_gb.dict',
'js/imes/latin/dictionaries/en_us.dict',
'js/imes/latin/dictionaries/es.dict',
'js/imes/latin/dictionaries/eu.dict',
'js/imes/latin/dictionaries/fr.dict',
'js/imes/latin/dictionaries/ga.dict',
'js/imes/latin/dictionaries/gd.dict',
'js/imes/latin/dictionaries/gl.dict',
'js/imes/latin/dictionaries/gv.dict',
'js/imes/latin/dictionaries/hr.dict',
'js/imes/latin/dictionaries/hu.dict',
'js/imes/latin/dictionaries/it.dict',
'js/imes/latin/dictionaries/lt.dict',
'js/imes/latin/dictionaries/lv.dict',
'js/imes/latin/dictionaries/nb.dict',
'js/imes/latin/dictionaries/nl.dict',
'js/imes/latin/dictionaries/pl.dict',
'js/imes/latin/dictionaries/pt_br.dict',
'js/imes/latin/dictionaries/pt_pt.dict',
'js/imes/latin/dictionaries/ro.dict',
'js/imes/latin/dictionaries/ru.dict',
'js/imes/latin/dictionaries/sk.dict',
'js/imes/latin/dictionaries/sq.dict',
'js/imes/latin/dictionaries/sr-Cyrl.dict',
'js/imes/latin/dictionaries/sr-Latn.dict',
'js/imes/latin/dictionaries/sv.dict',
'js/imes/latin/dictionaries/tr.dict',
'js/imes/latin/dictionaries/af.dict'
];
var checkList = [].concat(layouts, imes, dicts);
checkList.forEach(function(path) {
helper.checkFileInZip(zipPath, path, appDirPath + '/' + path);
});
// Verify inputs entry in manifest
var zip = new AdmZip(zipPath);
var entry = zip.getEntry('manifest.webapp');
var manifest = JSON.parse(zip.readAsText(entry));
var inputKeysInManifest = Object.keys(manifest.inputs);
assert.deepEqual(inputKeysInManifest.sort(),
[].concat(layoutIds, 'number', 'emoji').sort());
// Verify dictionary config
var dictJSON = JSON.parse(fs.readFileSync(
appDirPath +
'/test/build/integration/resources/' +
'all-layout-make-layouts.json'));
checkLayoutsJSON(zipPath, dictJSON);
done();
});
});
// Build with all layouts with no dictionaries
// (preload ko layout here to avoid empty check.)
test('APP=keyboard GAIA_KEYBOARD_LAYOUTS=ko ' +
'GAIA_KEYBOARD_DOWNLOADABLE_LAYOUTS=noPreloadDictRequired make',
function(done) {
var cmd = 'APP=keyboard GAIA_KEYBOARD_LAYOUTS=ko ' +
'GAIA_KEYBOARD_DOWNLOADABLE_LAYOUTS=noPreloadDictRequired make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var config = JSON.parse(process.env.BUILD_CONFIG);
var zipPath = path.join(process.cwd(), 'profile',
'webapps', 'keyboard.gaiamobile.org', 'application.zip');
var appDirPath = config.GAIA_DIR + '/apps/keyboard';
// For this test, we verify there isn't any dictionary
// in the zip.
var zip = new AdmZip(zipPath);
var entries = zip.getEntries();
var imePath = 'js/imes';
var sizeLimit = (1 << 10) * 100; // 100K
entries.forEach(function(entry) {
if (entry.entryName.substr(0, imePath.length) !== imePath) {
return;
}
var fileSize = entry.getData().length;
assert.isTrue(fileSize < sizeLimit,
'IME file is larger than non-dictionary limit: ' + entry.entryName +
', size: ' + fileSize + ' bytes.');
});
// Verify dictionary config
var dictJSON = JSON.parse(fs.readFileSync(
appDirPath +
'/test/build/integration/resources/' +
'no-preload-dict-required-make-layouts.json'));
checkLayoutsJSON(zipPath, dictJSON);
done();
});
});
// Build default layouts with only en dictionary, and extra IMEs
test('APP=keyboard GAIA_KEYBOARD_LAYOUTS=en ' +
'GAIA_KEYBOARD_DOWNLOADABLE_LAYOUTS=' +
'en,pt-BR,es,de,fr,fr-CA,pl,ko,zh-Hans-Pinyin,en-Dvorak make',
function(done) {
var cmd = 'APP=keyboard GAIA_KEYBOARD_LAYOUTS=en ' +
'GAIA_KEYBOARD_DOWNLOADABLE_LAYOUTS=' +
'en,pt-BR,es,de,fr,fr-CA,pl,ko,zh-Hans-Pinyin,en-Dvorak make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var config = JSON.parse(process.env.BUILD_CONFIG);
var zipPath = path.join(process.cwd(), 'profile',
'webapps', 'keyboard.gaiamobile.org', 'application.zip');
var appDirPath = config.GAIA_DIR + '/apps/keyboard';
var layoutIds = config.GAIA_KEYBOARD_LAYOUTS.split(',').sort()
.filter(function(layoutId) {
// Exclude noIncludeInExpandLayoutIdSet layouts
return (['emoji'].indexOf(layoutId) === -1);
});
var layouts = layoutIds.map(function(layout) {
return 'js/layouts/' + layout + '.js';
});
// Unfortunately we can only whilelist files here because we shouldn't
// throw error if excluded files are missing (e.g. README).
var imes = [
'js/imes/jshangul/jshangul.js',
'js/imes/jspinyin/empinyin_files.data',
'js/imes/jspinyin/empinyin_files.js',
'js/imes/jspinyin/jspinyin.js',
'js/imes/jspinyin/libpinyin.js',
'js/imes/jspinyin/worker.js',
'js/imes/latin/latin.js',
'js/imes/latin/predictions.js',
'js/imes/latin/worker.js'
];
var dicts = [
'js/imes/latin/dictionaries/es.dict',
'js/imes/latin/dictionaries/fr.dict',
'js/imes/latin/dictionaries/pl.dict',
'js/imes/latin/dictionaries/pt_br.dict'
];
var checkList = [].concat(layouts, imes);
checkList.forEach(function(path) {
helper.checkFileInZip(zipPath, path, appDirPath + '/' + path);
});
// Verify inputs entry in manifest
var zip = new AdmZip(zipPath);
var entry = zip.getEntry('manifest.webapp');
var manifest = JSON.parse(zip.readAsText(entry));
var inputKeysInManifest = Object.keys(manifest.inputs);
// Only layouts with dictionaries should be declaried.
// Noted that en-Dvorak use the same dictionary as en so it should also
// be declaried.
assert.deepEqual(inputKeysInManifest.sort(),
['en', 'en-Dvorak', 'ko', 'number', 'zh-Hans-Pinyin']);
// Verify dictionaries are not built (except en_us.dict)
dicts.forEach(function(dict) {
var entry = zip.getEntry(dict);
assert.equal(entry, null, 'Dictionary should not be built: ' + dict);
});
// Verify dictionary config
var dictJSON = JSON.parse(fs.readFileSync(
appDirPath +
'/test/build/integration/resources/' +
'default-make-en-dict-layouts.json'));
checkLayoutsJSON(zipPath, dictJSON);
done();
});
});
});
suite('Keyboard settings building tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
// parse settings.html and return domDoc.
var getSettingsDomDoc = function() {
var zipPath = path.join(process.cwd(), 'profile',
'webapps', 'keyboard.gaiamobile.org', 'application.zip');
// Verify settings.html content in manifest
var zip = new AdmZip(zipPath);
var entry = zip.getEntry('settings.html');
return jsdom(zip.readAsText(entry));
};
// return an array of <scripts> tag in <head>
var getScriptsFromDomDoc = function(domDoc) {
// We don't have Array.from in our node version, so use an old way to
// convert HTMLCollections to array
return Array.prototype.slice.call(
domDoc.head.getElementsByTagName('script'));
};
suite('For handwriting', function() {
// return an array of <sections> in the general panel
var getSectionsFromGeneralPanel = function(domDoc) {
return Array.prototype.slice.call(
domDoc.querySelectorAll('#general-container > section'));
};
// default: there shouldn't be handwriting elements in resulting file
test('APP=keyboard make', function(done) {
var cmd = 'APP=keyboard make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var settingsDOMDoc = getSettingsDomDoc();
assert.isTrue(getScriptsFromDomDoc(settingsDOMDoc).every(function(elem){
return elem.src !== 'js/settings/handwriting_settings_view.js';
}), 'No script should include handwriting_settings_view.js');
assert.isTrue(
getSectionsFromGeneralPanel(settingsDOMDoc).every(function(elem){
return elem.id !== 'handwriting-settings';
}), 'No section in general panel should include handwriting settings');
done();
});
});
test('GAIA_KEYBOARD_LAYOUTS=zh-Hans-Handwriting APP=keyboard make',
function(done) {
var cmd = 'GAIA_KEYBOARD_LAYOUTS=zh-Hans-Handwriting APP=keyboard make';
helper.exec(cmd, function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var settingsDOMDoc = getSettingsDomDoc();
assert.isTrue(getScriptsFromDomDoc(settingsDOMDoc).some(function(elem){
return elem.src === 'js/settings/handwriting_settings_view.js';
}), 'Some script should include handwriting_settings_view.js');
assert.isTrue(
getSectionsFromGeneralPanel(settingsDOMDoc).some(function(elem){
return elem.id === 'handwriting-settings';
}),
'Some section in general panel should include handwriting settings');
done();
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,146 @@
[
{
"id": "de",
"name": "Deutsch",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 2344627,
"preloaded": false,
"dictFilename": "de.dict",
"dictFilePath": "dictionaries/de.dict"
},
{
"id": "en",
"name": "English",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1451390,
"preloaded": true,
"dictFilename": "en_us.dict",
"dictFilePath": "dictionaries/en_us.dict"
},
{
"id": "en-Dvorak",
"name": "Dvorak",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1451390,
"preloaded": true,
"dictFilename": "en_us.dict",
"dictFilePath": "dictionaries/en_us.dict"
},
{
"id": "es",
"name": "Español",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1564842,
"preloaded": false,
"dictFilename": "es.dict",
"dictFilePath": "dictionaries/es.dict"
},
{
"id": "fr",
"name": "Français",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1874745,
"preloaded": false,
"dictFilename": "fr.dict",
"dictFilePath": "dictionaries/fr.dict"
},
{
"id": "fr-CA",
"name": "Français (Canadien)",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1874745,
"preloaded": false,
"dictFilename": "fr.dict",
"dictFilePath": "dictionaries/fr.dict"
},
{
"id": "ko",
"name": "한국어",
"imEngineId": "jshangul",
"types": [
"email",
"text",
"url"
],
"dictFileSize": 9562,
"preloaded": true
},
{
"id": "pl",
"name": "Polski",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1644422,
"preloaded": false,
"dictFilename": "pl.dict",
"dictFilePath": "dictionaries/pl.dict"
},
{
"id": "pt-BR",
"name": "Português (do Brasil)",
"imEngineId": "latin",
"types": [
"email",
"password",
"text",
"url"
],
"dictFileSize": 1524964,
"preloaded": false,
"dictFilename": "pt_br.dict",
"dictFilePath": "dictionaries/pt_br.dict"
},
{
"id": "zh-Hans-Pinyin",
"name": "拼音",
"imEngineId": "jspinyin",
"types": [
"email",
"text",
"url"
],
"dictFileSize": 2139624,
"preloaded": true
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"test",
"setup",
"marionette"
]
}

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Keyboard Test App</title>
<script type="text/javascript" src="js/main.js"></script>
<link rel="stylesheet" type="text/css" href="style/keyboard.css" />
</head>
<body>
<div id="keyboard" class="ime-test-app">
<button id="sendKey" class="key">Send Key: \o/ </button>
<button id="switchLayout" class="key">Switch layout</button>
</div>
</body>
</html>

View file

@ -0,0 +1,73 @@
'use strict';
/* global KeyEvent */
var inputContext = null;
var keyboardElement;
function init() {
keyboardElement = document.getElementById('keyboard');
window.navigator.mozInputMethod.oninputcontextchange = function() {
inputContext = navigator.mozInputMethod.inputcontext;
resizeWindow();
};
window.addEventListener('resize', resizeWindow);
keyboardElement.addEventListener('mousedown', function onMouseDown(evt) {
// Prevent loosing focus to the currently focused app
// Otherwise, right after mousedown event, the app will receive a focus event.
evt.preventDefault();
});
var sendKeyElement = document.getElementById('sendKey');
sendKeyElement.addEventListener('click', function sendKeyHandler() {
var testString = '\\o/';
for (var i = 0; i < testString.length; i++) {
sendKey(testString.charCodeAt(i));
}
});
var switchElement = document.getElementById('switchLayout');
switchElement.addEventListener('click', function switchHandler() {
var mgmt = navigator.mozInputMethod.mgmt;
mgmt.next();
});
// long press to trigger IME menu
var menuTimeout = 0;
switchElement.addEventListener('touchstart', function longHandler() {
menuTimeout = window.setTimeout(function menuTimeout() {
var mgmt = navigator.mozInputMethod.mgmt;
mgmt.showAll();
}, 700);
});
switchElement.addEventListener('touchend', function longHandler() {
clearTimeout(menuTimeout);
});
}
function resizeWindow() {
window.resizeTo(window.innerWidth, keyboardElement.clientHeight);
}
function sendKey(keyCode) {
switch (keyCode) {
case KeyEvent.DOM_VK_BACK_SPACE:
case KeyEvent.DOM_VK_RETURN:
if (inputContext) {
inputContext.sendKey(keyCode, 0, 0);
}
break;
default:
if (inputContext) {
inputContext.sendKey(0, keyCode, 0);
}
break;
}
}
window.addEventListener('load', init);

View file

@ -0,0 +1,19 @@
'use strict';
/* global MozActivity */
document.getElementById('back').addEventListener('click', function() {
document.addEventListener('visibilitychange', function() {
// Close ourself after the activity transition is completed.
window.close();
});
var activity = new MozActivity({
name: 'configure',
data: {
target: 'device'
}
});
activity.onerror = function() {};
});

View file

@ -0,0 +1,25 @@
{
"name": "LOL Keyboard",
"description": "An useless virtual keyboard that sends \\o/.",
"type": "privileged",
"role": "input",
"launch_path": "/settings.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"input": {
"description": "Send key events and characters to user focused input fields."
}
},
"inputs": {
"lol": {
"launch_path": "/index.html",
"name": "\\o/",
"description": "LOL layout",
"types": ["text", "number"]
}
},
"default_locale": "en-US"
}

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>LOL Keyboard Settings</title>
<script defer src="./js/settings.js"></script>
<style>
button {
font-size: 2em;
}
</style>
</head>
<body>
<h1>LOL Keyboard Settings</h1>
<p>Nothing to see here.</p>
<button id="back">&lt; Back</button>
</body>
</html>

View file

@ -0,0 +1,25 @@
html {
font-size: 10px;
pointer-events: none;
}
body {
margin: 0;
}
#keyboard {
width: 100%;
height: 20rem;
position: absolute;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
pointer-events: auto;
}
.key {
background-color: #333;
font-size: 2.4rem;
border: 0.2rem solid gray;
color: white;
margin: 0.2rem;
}

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Keyboard APP Test</title>
<script defer src="script.js"></script>
</head>
<body class="keyboard-test-app">
<div>
<label for="textInput">Text</label>
<input type="text" id="textInput"/>
</div>
<div>
<label for="textareInput">Textarea</label>
<textarea id="textareaInput"></textarea>
</div>
<div>
<label for="urlInput">URL</label>
<input type="url" id="urlInput"/>
</div>
<div>
<label for="emailInput">Email</label>
<input type="email" id="emailInput"/>
</div>
<div>
<label for="passwordInput">Password</label>
<input type="password" id="passwordInput"/>
</div>
<div>
<label for="searchInput">Search</label>
<input type="search" id="searchInput"/>
</div>
<div>
<label for="numberInput">Number</label>
<input type="number" id="numberInput"/>
</div>
<div>
<label for="telInput">Tel</label>
<input type="tel" id="telInput"/>
</div>
<div>
<div id="nonInput" style="height: 100px;">
</div>
</div>
<button type="button" id="triggerPromptButton">Prompt</button>
<p id="promptResult"></p>
</body>
</html>

View file

@ -0,0 +1,9 @@
{
"name": "keyboard tests",
"description": "A keyboard testing app.",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
}
}

View file

@ -0,0 +1,9 @@
'use strict';
document.getElementById('triggerPromptButton').onclick = () => {
// setTimeout is needed so we don't block marionette itself.
window.setTimeout(() => {
var str = window.prompt('The quick brown fox jumps over a', '');
document.getElementById('promptResult').textContent = str;
});
};

View file

@ -0,0 +1,187 @@
/* global suite */
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
KeyboardSettings = require('./lib/keyboard_settings.js'),
assert = require('assert');
marionette('Keyboard Auto correction tests', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var keyboardSettings = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'lockscreen.enabled': false,
'ftu.manifestURL': null,
'keyboard.autocorrect': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardSettings = new KeyboardSettings(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
});
suite('AutoCorrect switch', function() {
setup(function() {
keyboardSettings.launch();
});
test('Auto correct switch should equal to settings context',
function() {
var currentSwitch = client.settings.get('keyboard.autocorrect');
keyboardSettings.switchTo();
// Toggle one time
keyboardSettings.clickAutoCorrectOption();
assert.equal(keyboardSettings.autoCorrect, !currentSwitch);
// Toggle again
keyboardSettings.clickAutoCorrectOption();
assert.equal(keyboardSettings.autoCorrect, !!currentSwitch);
});
});
suite('With autocorrect on', function() {
setup(function () {
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.tap();
keyboard.switchTo();
});
test('Predictive tests', function() {
// Test 1 - type first 7 letters of the expected word
// select suggestion will append a space.
var expectedWord = 'keyboard ';
keyboard.type(expectedWord.slice(0, 7));
keyboard.tapAutoCorrectWord();
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), expectedWord);
// Test 2 - tap second suggestion, then press space
keyboard.switchTo();
keyboard.type('Tes');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'keyboard Tes');
keyboard.switchTo();
keyboard.tapSuggestionWord('Tea');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'keyboard Tea ');
// Test 3 - type something with autocorrect and press space
keyboard.switchTo();
keyboard.type('ye ');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'keyboard Tea he ');
// Test 4 - autocorrect, dot and backspace
keyboard.switchTo();
keyboard.type('wot.');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'),
'keyboard Tea he wit.');
keyboard.switchTo();
keyboard.tapBackspaceKey();
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'keyboard Tea he wot');
});
// For Bug 1073870
test('Move caret before autocorrect', function() {
keyboard.type('Hello worl');
keyboardTestApp.switchTo();
client.executeScript(
'var el = document.activeElement;' +
'el.selectionStart = el.selectionEnd = 5;');
keyboard.switchTo();
keyboard.type(' ');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'Hello worl');
});
});
suite('With autocorrect off', function() {
setup(function () {
client.settings.set('keyboard.autocorrect', false);
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.tap();
keyboard.switchTo();
});
test('No autocorrect while typing', function() {
keyboard.type('Tes');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'Tes');
keyboard.switchTo();
keyboard.tapSuggestionWord('Tea');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'Tea ');
keyboard.switchTo();
keyboard.type('ye.');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'Tea ye.');
keyboard.switchTo();
keyboard.tapBackspaceKey();
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), 'Tea ye');
});
});
});

View file

@ -0,0 +1,84 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Dimiss the keyboard', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var systemInputMgmt = null;
var system = null;
var actions;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
var client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
systemInputMgmt = client.loader.getAppClass('system', 'input_management');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
actions = client.loader.getActions();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.click();
keyboard.switchTo();
});
test('Longpressing the space bar should dismiss the keyboard', function() {
// The time needed for dismiss is 500ms.
keyboard.longPressSpaceBar(0.7);
client.waitFor(() => {
return !systemInputMgmt.keyboardFrameDisplayed();
});
assert.ok(true);
});
test('Longpressing for only 0.5 sec should not dimiss keyboard', function() {
keyboard.longPressSpaceBar(0.5);
var keyboardContainer = keyboard.currentPanel;
assert.ok(keyboardContainer.displayed());
});
test('Click on a non-input field, should dimiss keyboard', function() {
keyboardTestApp.switchTo();
keyboardTestApp.nonInputArea.click();
client.waitFor(() => {
return !systemInputMgmt.keyboardFrameDisplayed();
});
assert.ok(true);
});
test('Pressing [Home] button should dimiss keyboard', function() {
system.tapHome();
client.waitFor(() => {
return !systemInputMgmt.keyboardFrameDisplayed();
});
assert.ok(true);
});
});

View file

@ -0,0 +1,81 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Email keyboard input tests', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'lockscreen.enabled': false,
'ftu.manifestURL': null
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.emailInput.tap();
keyboard.switchTo();
});
test('Should be email layout', function() {
var atSymbol = '@';
var commaSymbol = ',';
assert.equal(keyboard.isKeyPresent(atSymbol), true);
assert.equal(keyboard.isKeyPresent(commaSymbol), false);
});
test('Type post@mydomain.com', function() {
keyboard.type('post');
keyboardTestApp.switchTo();
keyboardTestApp.emailInput.tap();
keyboard.switchTo();
keyboard.type('@');
keyboard.type('mydomain.com');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.emailInput.getAttribute('value'), 'post@mydomain.com');
});
test('Type post123@mydomain.com', function() {
keyboard.type('post123');
keyboard.type('@');
keyboard.type('mydomain.com');
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.emailInput.getAttribute('value'),
'post123@mydomain.com');
});
});

View file

@ -0,0 +1,119 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Launch Keyboard APP', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.click();
keyboard.switchTo();
});
test('Should show lowercase layout', function() {
// XXX: Workaround
// To get the #keyboard element to instead of the body element.
// The value of `client.findElement('body').displayed()` could not be true
// when the keyboard app is show up in the screen currently.
// Please refer to http://bugzil.la/995865.
var keyboardContainer = keyboard.currentPanel;
assert.ok(keyboardContainer.displayed());
var shiftKey = keyboard.shiftKey;
assert.ok(shiftKey.getAttribute('aria-pressed') === 'false');
var alphaKey = keyboard.getKey('a');
assert.ok(alphaKey.displayed());
});
test('Switch between inputs w/o waiting for layout loading', function() {
var keyboardContainer = keyboard.currentPanel;
assert.ok(keyboardContainer.displayed());
var shiftKey = keyboard.shiftKey;
var alphaKey = keyboard.getKey('a');
assert.ok(shiftKey.getAttribute('aria-pressed') === 'false');
assert.ok(alphaKey.displayed());
// Switch to test app frame.
keyboardTestApp.switchTo();
// Focus on the 2nd input
keyboardTestApp.numberInput.click();
// Without waiting for the keyboard, focus the 3rd input
keyboardTestApp.textareaInput.click();
// Switch back to keyboard
keyboard.switchTo();
// Should remain, or switched back to alpha keyboard.
client.waitFor(() => {
return alphaKey.displayed();
});
// Since the 3rd input is a <textarea>,
// we should reach upper case at this point.
client.waitFor(() => {
return (shiftKey.getAttribute('aria-pressed') === 'true');
});
});
test('Keys and buttons should be have aria-label set', function() {
var returnKey = keyboard.returnKey;
client.waitFor(() => {
return (returnKey.getAttribute('aria-label') === 'return');
});
var dismissSuggestionsKey = keyboard.dismissSuggestionsKey;
client.waitFor(() => {
var label = dismissSuggestionsKey.getAttribute('aria-label');
return (label === 'Dismiss');
});
});
test('Touching the status bar should not dismiss keyboard', function() {
// Click on the status bar
client.switchToFrame();
var topPanel = client.findElement('#top-panel');
topPanel.click();
client.helper.wait(3000);
keyboard.switchTo();
var keyboardContainer = keyboard.currentPanel;
assert.ok(keyboardContainer.displayed());
});
});

View file

@ -0,0 +1,10 @@
{
"extends": "../../../../../.jshintrc",
"node": true,
"predef": [
"test",
"setup",
"marionette",
"client"
]
}

View file

@ -0,0 +1,34 @@
'use strict';
function Base(client, origin) {
this.client = client;
this.origin = origin;
this.elements = {};
}
Base.prototype = {
launch: function() {
this.client.apps.launch(this.origin);
this.switchTo();
},
switchTo: function() {
this.client.switchToFrame();
this.client.apps.switchToApp(this.origin);
this.waitForReady();
},
close: function() {
this.client.apps.close(this.origin);
},
waitForReady: function() {
this.client.helper.waitForElement('body');
}
};
module.exports = Base;

View file

@ -0,0 +1,48 @@
'use strict';
/*
* A helper module for the testing 3rd-party IME app.
*/
var Base = require('./base');
function ImeTestApp(client) {
Base.call(this, client, ImeTestApp.ORIGIN);
}
module.exports = ImeTestApp;
ImeTestApp.ORIGIN = 'app://imetestapp.gaiamobile.org';
ImeTestApp.MANIFEST_URL = 'app://imetestapp.gaiamobile.org/manifest.webapp';
ImeTestApp.Selectors = Object.freeze({
sendKeyButton: '#sendKey',
switchLayoutButton: '#switchLayout'
});
ImeTestApp.prototype = {
__proto__: Base.prototype,
get sendKeyButton() {
return this.client.helper.waitForElement(
ImeTestApp.Selectors.sendKeyButton);
},
get switchLayoutButton() {
return this.client.helper.waitForElement(
ImeTestApp.Selectors.switchLayoutButton);
},
switchTo: function() {
var systemInputMgmt = this.client.loader.getAppClass(
'system', 'input_management');
var imeKeyboard;
systemInputMgmt.waitForKeyboardFrameDisplayed();
systemInputMgmt.switchToActiveKeyboardFrame();
imeKeyboard = this.client.helper.waitForElement('#keyboard.ime-test-app');
this.client.waitFor(() => {
return imeKeyboard.displayed();
});
}
};

View file

@ -0,0 +1,340 @@
'use strict';
var Base = require('./base');
var Marionette = require('marionette-client');
function Keyboard(client) {
Base.call(this, client);
this.actions = new Marionette.Actions(client);
this.currentLayout = null;
}
module.exports = Keyboard;
Keyboard.Selectors = Object.freeze({
currentPanel:
'.keyboard-type-container[data-active]',
imeSwitchingKey:
'.keyboard-type-container[data-active] ' +
'.keyboard-key[data-keycode="-3"]',
backspaceKey:
'.keyboard-type-container[data-active] ' +
'.keyboard-key[data-keycode="8"]',
returnKey:
'.keyboard-type-container[data-active] ' +
'.keyboard-key[data-l10n-id="returnKey2"]',
spaceBarKey: '.keyboard-type-container[data-active]' +
' .keyboard-key[data-keycode="32"]',
dismissSuggestionsKey:
'.keyboard-type-container[data-active] ' +
'.dismiss-suggestions-button',
shiftKey:
'.keyboard-type-container[data-active] ' +
'button.keyboard-key[data-l10n-id="upperCaseKey2"]',
autoCorrectWord:
'.keyboard-type-container[data-active] ' +
'.autocorrect',
});
Keyboard.Templates = Object.freeze({
key:
'.keyboard-type-container[data-active] ' +
'button.keyboard-key[data-keycode="%s"], ' +
'.keyboard-type-container[data-active] ' +
'button.keyboard-key[data-keycode-upper="%s"]',
upperCaseKey:
'.keyboard-type-container[data-active] ' +
'button.keyboard-key[data-keycode-upper="%s"]',
pageSwitchingKey:
'.keyboard-type-container[data-active] ' +
'button.keyboard-key[data-target-page="%s"]',
suggestionKey:
'.keyboard-type-container[data-active] ' +
'.suggestions-container span[data-data="%s"]',
});
Keyboard.LONGPRESS_CHARS = Object.freeze({
'0': 'º',
'?': '¿',
'$': '€£¥',
'!': '¡',
'a': 'áàâäåãāæ',
'c': 'çćč',
'e': 'éèêëēę€ɛ',
'i': 'įīîìíï',
'l': '£ł',
'n': 'ńñ',
'o': 'ɵøœōôòóö',
's': 'ßśš$',
'u': 'ūûùúü',
'y': '¥ÿ',
'z': 'žźż',
'A': 'ÁÀÂÄÅÃĀÆ',
'C': 'ÇĆČ',
'E': 'ÉÈÊËĒĘ€Ɛ',
'I': 'ĮĪÎÌÍÏ',
'L': '£Ł',
'N': 'ŃÑ',
'O': 'ƟØŒŌÔÒÓÖ',
'S': 'ŚŠŞ',
'U': 'ŪÛÙÚÜ',
'Y': '¥Ÿ',
'Z': 'ŽŹŻ'
});
Keyboard.TypeGroupMap = Object.freeze({
'text': 'text',
'textarea': 'text',
'url': 'url',
'email': 'email',
'password': 'password',
'search': 'text',
'number': 'number',
'tel': 'number'
});
Keyboard.prototype = {
__proto__: Base.prototype,
get currentPanel() {
return this.client.findElement(Keyboard.Selectors.currentPanel);
},
get imeSwitchingKey() {
return this.client.findElement(Keyboard.Selectors.imeSwitchingKey);
},
get backspaceKey() {
return this.client.findElement(Keyboard.Selectors.backspaceKey);
},
get returnKey() {
return this.client.findElement(Keyboard.Selectors.returnKey);
},
get spaceBarKey() {
return this.client.findElement(Keyboard.Selectors.spaceBarKey);
},
get shiftKey() {
return this.client.findElement(Keyboard.Selectors.shiftKey);
},
get dismissSuggestionsKey() {
return this.client.findElement(Keyboard.Selectors.dismissSuggestionsKey);
},
get autoCorrectWord() {
return this.client.findElement(Keyboard.Selectors.autoCorrectWord);
},
getKey: function(char) {
var selector = Keyboard.Templates.key.replace(/%s/g, char.charCodeAt(0));
return this.client.findElement(selector);
},
getPageSwitchingKey: function(index) {
var selector = Keyboard.Templates.pageSwitchingKey.replace(/%s/g, index);
return this.client.findElement(selector);
},
getSuggestionKey: function(word) {
var selector = Keyboard.Templates.suggestionKey.replace(/%s/, word);
return this.client.findElement(selector);
},
getLongPressCharMiddleKey: function(char) {
var chars = Keyboard.LONGPRESS_CHARS;
for (var middleChar in chars) {
if (chars[middleChar].indexOf(char) !== -1) {
return middleChar;
}
}
return undefined;
},
getCurrentInputType: function() {
return this.client.executeScript(
'return window.wrappedJSObject.app.getBasicInputType();');
},
getCurrentInputMode: function() {
return this.client.executeScript(
'return window.wrappedJSObject.app.inputContext.inputMode;');
},
getCurrentPageIndex: function() {
return this.client.executeScript(
'return window.wrappedJSObject.app.layoutManager.currentPageIndex;');
},
getCurrentKeyboardLayout: function() {
var keyboardFrame;
this.client.switchToFrame();
keyboardFrame = this.client.findElement(
'#keyboards .inputWindow.active iframe');
this.currentLayout = keyboardFrame.getAttribute('data-frame-name');
this.switchTo();
return this.currentLayout;
},
getSymbolPageIndex: function(key) {
this.switchToPage(1);
if (this.isKeyPresent(key)) {
return 1;
}
return 2;
},
tapBackspaceKey: function(word) {
this.backspaceKey.tap();
},
tapAutoCorrectWord: function() {
this.client.waitFor(() => {
return this.autoCorrectWord.displayed();
});
this.autoCorrectWord.tap();
},
tapSuggestionWord: function(word) {
this.getSuggestionKey(word).tap();
},
isUpperCase: function() {
return this.client.executeScript(
'return ' +
'window.wrappedJSObject.app.upperCaseStateManager.isUpperCase;');
},
isElementPresent: function(selector) {
var pollClient = this.client.scope({ searchTimeout: 50 });
try {
pollClient.findElement(selector);
} catch (e) {
return false;
}
return true;
},
isKeyPresent: function(key) {
var keyTemplate = Keyboard.Templates.key;
return this.isElementPresent(keyTemplate.replace(/%s/g, key.charCodeAt(0)));
},
switchCase: function(isUpperCase) {
if (isUpperCase === undefined || this.isUpperCase() === isUpperCase) {
return;
}
this.shiftKey.tap();
this.client.waitFor(() => {
var expected = isUpperCase ? 'true' : 'false';
return (this.shiftKey.getAttribute('aria-pressed') === expected);
});
},
switchToPage: function(index) {
var pageIndex = this.getCurrentPageIndex();
if (index === undefined || pageIndex === index) {
return;
}
this.getPageSwitchingKey(index).tap();
this.client.waitFor(() => {
return (this.getCurrentPageIndex() === index);
});
this.waitForPageReady();
},
switchToKeyLayout: function(key) {
var page;
var upperCase;
var currentLayout = this.currentLayout || this.getCurrentKeyboardLayout();
if (['number'].indexOf(currentLayout) !== -1) {
return;
}
if (key >= '0' && key <= '9') {
page = 1;
} else if (key >= 'A' && key <='Z') {
page = 0;
upperCase = true;
} else if (key >= 'a' && key <= 'z') {
page = 0;
upperCase = false;
} else if (key === ' ' || key === '\u0008' || key === '\u000d') {
} else {
page = this.getSymbolPageIndex(key);
}
this.switchToPage(page);
this.switchCase(upperCase);
},
type: function(string) {
string.split('').forEach((char) => {
var middleChar = this.getLongPressCharMiddleKey(char);
var lookingForKey = middleChar || char;
var keyElement;
this.switchToKeyLayout(lookingForKey);
if (!middleChar) {
keyElement = this.getKey(char);
this.actions.wait(0.1).tap(keyElement).perform();
} else {
keyElement = this.getKey(middleChar);
this.actions
.press(keyElement).wait(1.5).perform()
.move(this.getKey(char)).wait(0.2).release().perform();
}
});
},
longPressSpaceBar: function(time) {
this.actions.longPress(this.spaceBarKey, time).perform();
},
switchTo: function() {
var systemInputMgmt = this.client.loader.getAppClass(
'system', 'input_management');
systemInputMgmt.waitForKeyboardFrameDisplayed();
systemInputMgmt.switchToActiveKeyboardFrame();
this.waitForPageReady();
},
waitForPageReady: function() {
this.client.waitFor(() => {
return this.currentPanel.displayed();
});
},
waitForKeyboardReady: function() {
this.client.switchToFrame();
this.client.waitFor(() => {
return this.client.findElement(
'#keyboards .inputWindow .browser-container');
});
}
};

View file

@ -0,0 +1,50 @@
'use strict';
var Base = require('./base.js');
function KeyboardSettings(client) {
this.client = client.scope({ searchTimeout: 500 });
Base.call(this, client, KeyboardSettings.ORIGIN);
}
module.exports = KeyboardSettings;
KeyboardSettings.ORIGIN = 'app://keyboard.gaiamobile.org';
KeyboardSettings.Selectors = Object.freeze({
autoCorrectCheckBox: '#cb-autoCorrect'
});
KeyboardSettings.prototype = {
__proto__: Base.prototype,
get autoCorrectCheckBox() {
return this.client.findElement(
KeyboardSettings.Selectors.autoCorrectCheckBox);
},
get autoCorrect() {
return this.autoCorrectCheckBox.scriptWith(function(ele) {
return ele.wrappedJSObject.checked;
});
},
clickAutoCorrectOption: function() {
var currentSwitch = this.client.settings.get('keyboard.autocorrect');
this.autoCorrectCheckBox.click();
this.client.waitFor(() => {
return (this.autoCorrect !== currentSwitch);
});
},
waitForReady: function() {
this.client.waitFor(() => {
return this.autoCorrectCheckBox.displayed();
});
}
};

View file

@ -0,0 +1,97 @@
'use strict';
var Base = require('./base');
function KeyboardTests(client) {
Base.call(this, client, KeyboardTests.ORIGIN);
}
module.exports = KeyboardTests;
KeyboardTests.ORIGIN = 'app://keyboardtestapp.gaiamobile.org';
KeyboardTests.Selector = Object.freeze({
textInput: '#textInput',
textareaInput: '#textareaInput',
urlInput: '#urlInput',
emailInput: '#emailInput',
passwordInput: '#passwordInput',
searchInput: '#searchInput',
numberInput: '#numberInput',
telInput: '#telInput',
nonInputArea: '#nonInput',
triggerPromptButton: '#triggerPromptButton',
promptResult: '#promptResult'
});
KeyboardTests.prototype = {
__proto__: Base.prototype,
get textInput() {
return this.client.findElement(KeyboardTests.Selector.textInput);
},
get textareaInput() {
return this.client.findElement(KeyboardTests.Selector.textareaInput);
},
get urlInput() {
return this.client.findElement(KeyboardTests.Selector.urlInput);
},
get emailInput() {
return this.client.findElement(KeyboardTests.Selector.emailInput);
},
get passwordInput() {
return this.client.findElement(KeyboardTests.Selector.passwordInput);
},
get searchInput() {
return this.client.findElement(KeyboardTests.Selector.searchInput);
},
get numberInput() {
return this.client.findElement(KeyboardTests.Selector.numberInput);
},
get telInput() {
return this.client.findElement(KeyboardTests.Selector.telInput);
},
get nonInputArea() {
return this.client.findElement(KeyboardTests.Selector.nonInputArea);
},
get triggerPromptButton() {
return this.client.findElement(KeyboardTests.Selector.triggerPromptButton);
},
get promptResult() {
return this.client.findElement(KeyboardTests.Selector.promptResult);
},
triggerPromptModalDialog: function() {
this.triggerPromptButton.tap();
},
waitForReady: function() {
var inputs = [
this.textInput,
this.textareaInput,
this.urlInput,
this.emailInput,
this.passwordInput,
this.searchInput,
this.numberInput,
this.telInput
];
this.client.waitFor(() => {
return inputs.every((ele) => {
return ele.displayed();
});
});
}
};

View file

@ -0,0 +1,74 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Number keyboard input tests', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'lockscreen.enabled': false,
'ftu.manifestURL': null
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.numberInput.tap();
keyboard.switchTo();
});
test('Should be number layout', function() {
assert.equal(
keyboard.getCurrentKeyboardLayout(), Keyboard.TypeGroupMap.number);
});
test('Alphabet should not present on keyboard', function() {
var testAlphabetKey = 'a';
assert.equal(keyboard.isKeyPresent(testAlphabetKey), false);
});
test('Type 1', function() {
var inputString = '1';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.numberInput.getAttribute('value'), inputString);
});
test('Type 7997979979', function() {
var inputString = '7997979979';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.numberInput.getAttribute('value'), inputString);
});
});

View file

@ -0,0 +1,134 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app');
var ImeTestApp = require('./lib/ime_test_app');
var Keyboard = require('./lib/keyboard');
var assert = require('assert');
marionette('Switch Keyboard App', function() {
var keyboardTestApp = null;
var keyboard = null;
var systemInputMgmt = null;
var imeTestApp = null;
var client = null;
var actions = null;
var apps = {};
var system = null;
// Pre-install keyboard testing app
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
// And a testing 3rd-party IME app
apps[ImeTestApp.ORIGIN] = __dirname + '/apps/imetestapp';
var keyboardSettings = {};
keyboardSettings[Keyboard.MANIFEST_URL] = {
'en': true,
'number': true
};
// Enable a testing IME app
keyboardSettings[ImeTestApp.MANIFEST_URL] = {
'lol': true
};
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'keyboard.enabled-layouts': keyboardSettings
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
/*
* To check the 3rd-party IME is shown.
*/
function check3rdPartyIme() {
// switch back to system
client.switchToFrame();
// wait for the 2nd keyboard is loaded
systemInputMgmt.ensureInputWindowCount(2);
systemInputMgmt.switchToActiveKeyboardFrame();
assert.ok(imeTestApp.sendKeyButton.displayed());
}
setup(function() {
actions = client.loader.getActions();
systemInputMgmt = client.loader.getAppClass('system', 'input_management');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
imeTestApp = new ImeTestApp(client);
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.urlInput.click();
keyboard.switchTo();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.click();
// Wait for the keyboard pop up and switch to it
keyboard.switchTo();
});
test('Checking the switching IME function is available', function() {
var imeSwitchingKey = keyboard.imeSwitchingKey;
assert.ok(imeSwitchingKey.displayed());
// Click to switch to next IME
imeSwitchingKey.click();
check3rdPartyIme();
});
test('Long press to show the IME menu', function() {
var imeSwitchingKey = keyboard.imeSwitchingKey;
// Long press to show IME menu
actions.longPress(imeSwitchingKey, 0.7).perform();
// switch back to system
client.switchToFrame();
var imeMenu = systemInputMgmt.imeMenu;
assert.ok(imeMenu.displayed());
// select the 3rd-party IME
systemInputMgmt.selectImeOption(1);
check3rdPartyIme();
});
test('Drag down the utility tray', function() {
// Swipe to drag down the utility tray
systemInputMgmt.dragDownUtilityTray();
// Check the IME switching buttion in notification
var imeNotification = systemInputMgmt.imeNotification;
client.waitFor(function() {
return imeNotification.displayed();
});
imeNotification.click();
assert.ok(systemInputMgmt.imeMenu.displayed());
//XXX: Wait for the previous keyboard to hide or we may not be able to
// switch to the next keyboard.
client.waitFor(function() {
return systemInputMgmt.keyboardFrameHidden();
});
// select the 3rd-party IME
systemInputMgmt.selectImeOption(1);
check3rdPartyIme();
});
});

View file

@ -0,0 +1,119 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Dialog = require('../../../system/test/marionette/lib/dialog'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Text keyboard input tests', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var systemInputMgmt = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'lockscreen.enabled': false,
'ftu.manifestURL': null
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
systemInputMgmt = client.loader.getAppClass('system', 'input_management');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.tap();
keyboard.switchTo();
});
test('Type abc', function() {
var inputString = 'abc';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), inputString);
});
test('Type multiple alphabet', function() {
var inputString = 'aGDsaIabcmvljdDFDFDDs śZîd';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), inputString);
});
test('Type number and then alphabet', function() {
var inputString = '123abc';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), inputString);
});
test('Type alphabet and then number', function() {
var inputString = 'abc123';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), inputString);
});
test('Type symbols', function() {
var inputString = 'aG1 !@#$&*-_/(),.:;"\'?[]{}<>+=`^~|\\%';
keyboard.type(inputString);
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textInput.getAttribute('value'), inputString);
});
var dialog;
test('Type into an window.prompt() dialog and hit enter', function() {
// Hide keyboard before click prompt
keyboardTestApp.switchTo();
keyboardTestApp.nonInputArea.tap();
client.waitFor(() => {
return !systemInputMgmt.keyboardFrameDisplayed();
});
dialog = new Dialog(client);
keyboardTestApp.switchTo();
keyboardTestApp.triggerPromptModalDialog();
client.switchToFrame();
dialog.promptInput.tap();
keyboard.switchTo();
keyboard.type('lazy dog.');
keyboard.getKey('\u000d').tap();
keyboardTestApp.switchTo();
assert.equal('lazy dog.', keyboardTestApp.promptResult.text());
});
});

View file

@ -0,0 +1,100 @@
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app'),
Keyboard = require('./lib/keyboard'),
assert = require('assert');
marionette('Textarea keyboard input tests', function() {
var apps = {};
var keyboardTestApp = null;
var keyboard = null;
var client = null;
var system = null;
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'lockscreen.enabled': false,
'ftu.manifestURL': null
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
keyboardTestApp = new KeyboardTestApp(client);
keyboard.waitForKeyboardReady();
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.textareaInput.tap();
keyboard.switchTo();
});
test('Type Abc', function() {
var inputString = 'Abc';
keyboard.type(inputString.substring(0, 1));
var shiftKey = keyboard.shiftKey;
client.waitFor(function() {
return (shiftKey.getAttribute('aria-pressed') === 'false');
});
keyboard.type(inputString.substring(1));
keyboardTestApp.switchTo();
assert.equal(
keyboardTestApp.textareaInput.getAttribute('value'), inputString);
});
test('Double tapping space bar', function() {
var inputString = 'Aa';
keyboard.type(inputString);
client.waitFor(() => {
return keyboard.autoCorrectWord.displayed();
});
// type 2 spaces
keyboard.type(' ');
// Switch to test app frame.
keyboardTestApp.switchTo();
assert.equal(
'As. ', keyboardTestApp.textareaInput.getAttribute('value'));
});
test('Tap space bar and then wait for a while before tapping again',
function() {
var inputString = 'Aa';
keyboard.type(inputString);
client.waitFor(() => {
return keyboard.autoCorrectWord.displayed();
});
// type one space and then tap again after a while
keyboard.type(' ');
// The timeout for double tapping is set as 700ms.
client.helper.wait(800);
keyboard.type(' ');
// Switch to test app frame.
keyboardTestApp.switchTo();
assert.equal('As ', keyboardTestApp.textareaInput.getAttribute('value'));
});
});

View file

@ -0,0 +1,127 @@
/*
* This test is to ensure that system's keyboard manager would
* correctly show the built-in keyboard app after the previous activated
* keyboard has been removed.
*/
'use strict';
var KeyboardTestApp = require('./lib/keyboard_test_app');
var ImeTestApp = require('./lib/ime_test_app');
var Keyboard = require('./lib/keyboard');
var assert = require('assert');
marionette('Show keyboard app after uninstallation', function() {
var keyboardTestApp = null;
var keyboard = null;
var systemInputMgmt = null;
var system = null;
var imeTestApp = null;
var appInstall = null;
var client = null;
var apps = {};
var confirmDialog = null;
// Pre-install keyboard testing app
apps[KeyboardTestApp.ORIGIN] = __dirname + '/apps/keyboardtestapp';
// And a testing 3rd-party IME app
apps[ImeTestApp.ORIGIN] = __dirname + '/apps/imetestapp';
var keyboardSettings = {};
keyboardSettings[Keyboard.MANIFEST_URL] = {
'en': true,
'number': true
};
// Enable a testing IME app
keyboardSettings[ImeTestApp.MANIFEST_URL] = {
'lol': true
};
client = marionette.client({
profile: {
apps: apps,
prefs: {
'focusmanager.testmode': true
},
settings: {
'keyboard.enabled-layouts': keyboardSettings
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
/*
* To check the 3rd-party IME is shown.
*/
function check3rdPartyIme() {
// switch back to system
client.switchToFrame();
// wait for the 2nd keyboard is loaded
systemInputMgmt.ensureInputWindowCount(2);
systemInputMgmt.switchToActiveKeyboardFrame();
return imeTestApp.sendKeyButton.displayed();
}
setup(function() {
systemInputMgmt = client.loader.getAppClass('system', 'input_management');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
keyboard = new Keyboard(client);
appInstall = client.loader.getAppClass('system', 'app_install');
confirmDialog = client.loader.getAppClass('system', 'confirm_dialog');
imeTestApp = new ImeTestApp(client);
keyboard.waitForKeyboardReady();
// create a keyboard test app
keyboardTestApp = new KeyboardTestApp(client);
keyboardTestApp.launch();
keyboardTestApp.switchTo();
keyboardTestApp.urlInput.click();
keyboard.switchTo();
keyboardTestApp.switchTo();
keyboardTestApp.textInput.click();
keyboard.switchTo();
keyboard.imeSwitchingKey.click();
// Click to switch to next IME
check3rdPartyIme();
// XXX: Blur the input field, otherwise the keyboard frame has higher
// z-index than dialog and we can't click it
systemInputMgmt.waitForKeyboardFrameDisplayed();
keyboardTestApp.switchTo();
keyboardTestApp.nonInputArea.click();
// Uninstall the current active IME
appInstall.uninstall(ImeTestApp.MANIFEST_URL);
client.switchToFrame();
confirmDialog.confirm('remove');
// Click the input field again to check the built-in keyboard
keyboardTestApp.switchTo();
// XXX: do something to remove the focus
keyboardTestApp.nonInputArea.click();
keyboardTestApp.textInput.click();
// Wait for the keyboard pop up and switch to it
keyboard.switchTo();
});
test('Fallback to built-in keyboard when the active IME has been ' +
'uninstalled', function() {
assert.ok(keyboard.currentPanel.displayed());
});
test('Should not show IME switching key after uninstallation', function() {
// Since the switching key is gone, we would show ',' on the first page
var commaKey = keyboard.getKey(',');
assert.ok(commaKey.displayed());
});
});

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,13 @@
'use strict';
/* global InputMethods */
InputMethods.foo = {
init: function(glue) {
this._glue = glue;
},
activate: sinon.stub(),
deactivate: sinon.stub(),
click: sinon.stub(),
stateChange: sinon.stub()
};

View file

@ -0,0 +1,11 @@
'use strict';
/* global Keyboards */
Keyboards.foo = {
keys: [
[
{ value: 'foo' }
]
]
};

View file

@ -0,0 +1,21 @@
'use strict';
/* global Keyboards */
/* This file exposes two layouts */
Keyboards.foo2 = {
keys: [
[
{ value: 'foo2' }
]
]
};
Keyboards.foo3 = {
keys: [
[
{ value: 'foo3' }
]
]
};

View file

@ -0,0 +1,490 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* globals InputMethods, KeyEvent */
'use strict';
requireApp('keyboard/test/unit/setup_engine.js');
requireApp('keyboard/js/imes/jspinyin/jspinyin.js');
suite('jspinyin', function() {
var NUMBER_OF_CANDIDATES_PER_ROW = 8;
var jspinyin;
var firstCandidate;
var firstPredict;
var dummyFunction = function() {};
var glue = {
path: '/js/imes/jspinyin',
sendCandidates: dummyFunction,
setComposition: dummyFunction,
endComposition: dummyFunction,
sendKey: dummyFunction,
sendString: dummyFunction,
setLayoutPage: dummyFunction,
setUpperCase: dummyFunction,
resetUpperCase: dummyFunction,
replaceSurroundingText: dummyFunction,
getNumberOfCandidatesPerRow: function() {
return NUMBER_OF_CANDIDATES_PER_ROW;
}
};
var fakeUserDict = [240, 222, 188, 10, 0, 2, 239, 0, 176, 1, 11, 140, 122,
102, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 252, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0,
0];
var fakeUserDictNew = [240, 222, 188, 10, 0, 2, 239, 0, 176, 1, 11, 140, 122,
102, 0, 2, 239, 0, 176, 1, 208, 103, 234, 83, 10];
function mockIndexedDB(args) {
var receivedData;
var request = {};
var transRequest = {};
var fakeDB = {
objectStoreNames: [1, 2],
createObjectStore: dummyFunction,
deleteObjectStore: dummyFunction,
transaction: sinon.stub(),
objectStore: sinon.stub(),
get: sinon.stub(),
put: dummyFunction,
close: dummyFunction
};
var evt = {
target: {
result: fakeDB
}
};
var evtError = {
target: {
errorCode: 123,
result: {
name: 'test'
}
}
};
sinon.stub(window.indexedDB, 'open');
window.indexedDB.open.returns(request);
fakeDB.transaction.returns(fakeDB);
fakeDB.objectStore.returns(fakeDB);
fakeDB.get.returns(transRequest);
sinon.stub(fakeDB, 'put', function(data) {
receivedData = data;
return transRequest;
});
if (args.testing) {
args.testing();
}
function indexedDBActions() {
if (!args.ignoreUndefinedHandler) {
assert.isFunction(request.onupgradeneeded);
assert.isFunction(request.onsuccess);
}
if (args.openDBFailed) {
if (request.onerror) {
request.onerror(evtError);
}
if (args.callback) {
args.callback(receivedData);
}
return;
}
if (request.onupgradeneeded) {
request.onupgradeneeded(evt);
}
if (request.onsuccess) {
request.onsuccess(evt);
}
if (!args.ignoreUndefinedHandler) {
assert.isFunction(transRequest.onsuccess);
}
if (args.transactionFailed) {
if (transRequest.onerror) {
transRequest.onerror(evtError);
}
if (args.callback) {
args.callback(receivedData);
}
return;
}
if (transRequest.onsuccess) {
transRequest.onsuccess({
target: {
result: {
content: fakeUserDict
}
}
});
if (args.callback) {
args.callback(receivedData);
}
}
}
if (args.waitForHandler) {
var indexedDBActionsTimer = setInterval(function() {
if (request.onsuccess) {
indexedDBActions();
clearInterval(indexedDBActionsTimer);
}
}, 100);
} else {
indexedDBActions();
}
}
suiteTeardown(function() {
jspinyin.uninit();
assert.isNull(jspinyin._uninitTimer);
delete InputMethods.jspinyin;
});
teardown(function() {
if (window.indexedDB.open.restore) {
window.indexedDB.open.restore();
}
});
test('load', function() {
assert.isDefined(InputMethods.jspinyin);
jspinyin = InputMethods.jspinyin;
});
test('init', function() {
jspinyin.init(glue);
assert.equal(jspinyin._glue.path, glue.path);
});
test('do something before activate', function(done) {
jspinyin.click(KeyEvent.DOM_VK_RETURN);
jspinyin.select('', 0);
jspinyin.getMoreCandidates(
0,
NUMBER_OF_CANDIDATES_PER_ROW,
function(list) {
done(function() {
assert.isNull(list);
});
}
);
});
test('activate', function(done) {
this.sinon.stub(jspinyin, '_start', function() {
done(function() {
assert.equal(jspinyin._keypressQueue[0], KeyEvent.DOM_VK_RETURN);
assert.isNull(jspinyin._uninitTimer);
jspinyin._resetKeypressQueue();
});
});
mockIndexedDB({
testing: function() {
jspinyin._uninitTimer = setTimeout(function() {
throw 'Should not call this function.';
}, 0);
jspinyin.activate('zh-Hans', { type: 'text' }, {});
}
});
});
test('activate twice', function() {
this.sinon.stub(jspinyin, '_start');
mockIndexedDB({
ignoreUndefinedHandler: true,
testing: function() {
jspinyin._uninitTimer = setTimeout(function() {
throw 'Should not call this function.';
}, 0);
jspinyin.activate('zh-Hans', { type: 'textarea' }, {});
}
});
});
test('click \'z\'', function(done) {
var len;
this.sinon.stub(glue, 'sendCandidates', function(list) {
firstCandidate = list[0];
len = list.length;
});
this.sinon.stub(glue, 'setComposition', function(symbols, cursor) {
if (glue.sendCandidates.called) {
done(function() {
assert.equal(symbols, 'z');
assert.deepEqual(firstCandidate, ['在', 0]);
assert.equal(len, NUMBER_OF_CANDIDATES_PER_ROW + 1);
});
}
});
jspinyin.click('z'.charCodeAt(0));
});
test('get more candidates', function(done) {
jspinyin.getMoreCandidates(
NUMBER_OF_CANDIDATES_PER_ROW,
NUMBER_OF_CANDIDATES_PER_ROW * 12,
function(list) {
done(function() {
assert.deepEqual(list[0], ['真', 8]);
assert.deepEqual(list[list.length - 1], ['忠', 103]);
assert.equal(list.length, NUMBER_OF_CANDIDATES_PER_ROW * 12);
});
}
);
});
test('select first candidate and get predicts', function(done) {
var endCompositionString;
this.sinon.stub(glue, 'endComposition', function(text) {
if (text) {
endCompositionString = text;
}
});
this.sinon.stub(glue, 'sendCandidates', function(list) {
firstPredict = list[0];
done(function() {
assert.equal(endCompositionString, firstCandidate[0]);
assert.deepEqual(firstPredict, ['线', 0]);
assert.equal(list.length, NUMBER_OF_CANDIDATES_PER_ROW + 1);
});
});
jspinyin.select(firstCandidate[0], firstCandidate[1]);
});
test('get more predicts', function(done) {
jspinyin.getMoreCandidates(
NUMBER_OF_CANDIDATES_PER_ROW,
NUMBER_OF_CANDIDATES_PER_ROW * 12,
function(list) {
done(function() {
assert.deepEqual(list[0], ['乎', 8]);
assert.deepEqual(list[list.length - 1], ['新窗口', 97]);
assert.equal(list.length, 90);
});
}
);
});
test('select first predict', function(done) {
this.sinon.stub(jspinyin, '_start');
this.sinon.stub(glue, 'endComposition', function(text) {
done(function() {
assert.equal(text, firstPredict[0]);
});
});
jspinyin.select(firstPredict[0], firstPredict[1]);
});
test('empty', function(done) {
this.sinon.spy(glue, 'endComposition');
this.sinon.stub(glue, 'sendCandidates', function(list) {
if (glue.endComposition.called) {
done(function() {
assert.isTrue(glue.endComposition.calledWith(undefined));
assert.equal(list.length, 0);
});
}
});
jspinyin.empty();
});
test('input the phrase in userdict from indexedDB', function(done) {
jspinyin.click('m'.charCodeAt(0));
jspinyin.click('o'.charCodeAt(0));
jspinyin.click('u'.charCodeAt(0));
jspinyin.click('z'.charCodeAt(0));
jspinyin.click('h'.charCodeAt(0));
jspinyin.click('i'.charCodeAt(0));
jspinyin.click(KeyEvent.DOM_VK_RETURN);
this.sinon.stub(glue, 'endComposition', function(text) {
if (!text) {
done(function() {
assert.isTrue(glue.endComposition.calledWith('谋智'));
});
}
});
});
test('create new phrase into userdict', function(done) {
this.sinon.stub(glue, 'endComposition', function(text) {
if (!text) {
done(function() {
assert.isTrue(glue.endComposition.calledWith('某只'));
});
}
});
this.sinon.stub(glue, 'setComposition', function(symbol, cursor) {
if (symbol == 'mou zhi') {
jspinyin.select('某', 1);
} else if (symbol == '某zhi') {
jspinyin.select('只', 1);
}
});
jspinyin.click('m'.charCodeAt(0));
jspinyin.click('o'.charCodeAt(0));
jspinyin.click('u'.charCodeAt(0));
jspinyin.click('z'.charCodeAt(0));
jspinyin.click('h'.charCodeAt(0));
jspinyin.click('i'.charCodeAt(0));
});
test('input the phrase in userdict that we just created', function(done) {
var isOk = false;
jspinyin.click('m'.charCodeAt(0));
jspinyin.click('o'.charCodeAt(0));
jspinyin.click('u'.charCodeAt(0));
jspinyin.click('z'.charCodeAt(0));
jspinyin.click('h'.charCodeAt(0));
jspinyin.click('i'.charCodeAt(0));
jspinyin.click(KeyEvent.DOM_VK_RETURN);
this.sinon.stub(glue, 'sendCandidates', function(list) {
if (list[1] && list[1][0] == '某只') {
isOk = true;
}
});
this.sinon.stub(glue, 'endComposition', function(text) {
if (!text) {
done(function() {
assert.isTrue(isOk);
});
}
});
});
test('click backspace to clear pendingSymbols', function(done) {
this.sinon.stub(glue, 'setComposition');
this.sinon.stub(glue, 'endComposition');
this.sinon.stub(glue, 'sendKey', function(keyCode) {
done(function() {
assert.equal(keyCode, KeyEvent.DOM_VK_BACK_SPACE);
assert.isTrue(glue.endComposition.called);
});
});
jspinyin.click('z'.charCodeAt(0));
// "A" means "'" for pinyin composition
jspinyin.click('A'.charCodeAt(0));
jspinyin.click(KeyEvent.DOM_VK_BACK_SPACE);
jspinyin.click(KeyEvent.DOM_VK_BACK_SPACE);
jspinyin.click(KeyEvent.DOM_VK_BACK_SPACE);
});
test('click a symbol', function(done) {
this.sinon.stub(glue, 'sendKey', function(keyCode) {
done(function() {
assert.equal(keyCode, '\''.charCodeAt(0));
});
});
jspinyin.click('\''.charCodeAt(0));
});
test('deactivate', function(done) {
this.sinon.spy(jspinyin, 'empty');
mockIndexedDB({
waitForHandler: true,
callback: function(data) {
done(function() {
assert.isTrue(jspinyin.empty.calledOnce);
assert.deepEqual(data.content.slice(0, fakeUserDictNew.length),
fakeUserDictNew);
});
},
testing: function() {
jspinyin.deactivate();
}
});
});
test('uninit', function() {
this.sinon.spy(jspinyin, 'empty');
jspinyin.uninit();
assert.isNull(jspinyin._uninitTimer);
assert.isTrue(jspinyin.empty.calledOnce);
});
test('activate (open indexedDB failed)', function(done) {
mockIndexedDB({
openDBFailed: true,
ignoreUndefinedHandler: true,
callback: function() {
done();
},
testing: function() {
jspinyin.activate('zh-Hans', { type: 'text' }, {});
}
});
});
test('activate (load indexedDB failed)', function(done) {
mockIndexedDB({
transactionFailed: true,
ignoreUndefinedHandler: true,
callback: function() {
done();
},
testing: function() {
jspinyin.activate('zh-Hans', { type: 'text' }, {});
}
});
});
// Bug 1146260
test.skip('deactivate (save indexedDB failed)', function(done) {
this.sinon.stub(jspinyin, '_start', function() {
window.indexedDB.open.restore();
setTimeout(function() {
mockIndexedDB({
waitForHandler: true,
transactionFailed: true,
ignoreUndefinedHandler: true,
callback: function() {
done();
},
testing: function() {
jspinyin.deactivate();
}
});
}, 500);
});
mockIndexedDB({
testing: function() {
jspinyin.activate('zh-Hans', { type: 'text' }, {});
}
});
});
});

View file

@ -0,0 +1,999 @@
'use strict';
/* global KeyboardEvent, KeyEvent, PAGE_INDEX_DEFAULT, PromiseStorage */
require('/js/shared/promise_storage.js');
window.PAGE_INDEX_DEFAULT = 0;
window.InputMethods = {};
suite('latin.js', function() {
var engine;
var glue;
// We pass the same workerStub because latin engine reuse the worker
// between activations.
var workerStub = {
onmessage: null,
postMessage: function() { /* to be stubbed between tests */}
};
// Load the script once and attach it to |engine|.
suiteSetup(function(done) {
require('/js/imes/latin/latin.js', function() {
engine = window.InputMethods.latin;
done();
});
});
setup(function() {
this.sinon.stub(workerStub, 'postMessage');
this.sinon.stub(window, 'Worker').returns(workerStub);
var stubPromiseStorage =
this.sinon.stub(Object.create(PromiseStorage.prototype));
this.sinon.stub(window, 'PromiseStorage')
.returns(stubPromiseStorage);
// object is what we'd used to resolve into (see getData below)
stubPromiseStorage.getItem.returns(Promise.resolve({user: 'user'}));
glue = {
mOutput: '',
mIsUpperCase: false,
mResolveSendKey: true,
sendKey: this.sinon.spy(function(keycode) {
if (!this.mResolveSendKey) {
return Promise.reject();
}
if (keycode === 8) { // backspace
this.mOutput =
this.mOutput.substring(0, this.mOutput.length - 1);
}
else {
this.mOutput += String.fromCharCode(keycode);
}
return Promise.resolve();
}),
sendCandidates: this.sinon.stub(),
setUpperCase: this.sinon.spy(function setUpperCase(state) {
this.mIsUpperCase = state.isUpperCase;
}),
setLayoutPage: this.sinon.stub(),
isCapitalized: this.sinon.spy(function isCapitalized() {
return this.mIsUpperCase;
}),
replaceSurroundingText: this.sinon.stub().returns(Promise.resolve()),
getData: this.sinon.stub().returns(Promise.resolve({}))
};
// We are not supposed to call init() more than once in the real setup,
// but we need to call it here, as we need to have the engine use a
// new glue object for each test.
//
// Moreover, for each of the tests, we will activate() the engine with
// desired conditions and deactivate on teardown().
engine.init(glue);
});
suite('Capitalization and punctuation behavior', function() {
// These tests verify that the latin input method is doing what it should.
// They pass input to the IM by calling its click() method, and verify that
// the input method sends back the expected output through the sendKey
// function that we pass it. The latin input method sends suggestions
// asynchronously, but other parts are synchronous
// The capitalization and punctuation behavior of the Latin IM depends
// on these variables:
//
// input field type
// input mode
// existing text in input field
// cursor position
// whether there is a selection
// Test each of these types
var types = [
'text',
'textarea',
'search',
'url',
'email'
];
// Test these input modes
var modes = [
'',
'verbatim',
'latin',
'latin-prose'
];
var contentStates = {
// empty input field
empty: { text: '', cursor: 0 },
// cursor is in the middle of a bunch of spaces
inSpace: { text: 'a b', cursor: 4},
// cursor is at the start, middle, or end of the input field
start: { text: 'word', cursor: 0 },
middle: { text: 'word', cursor: 2 },
end: { text: 'word', cursor: 4 },
// like the above, but all uppercase
startUppercase: { text: 'WORD', cursor: 0 },
middleUppercase: { text: 'WORD', cursor: 2 },
endUppercase: { text: 'WORD', cursor: 4 },
// cursor is at the start, middle, or end of a word in the middle
wordStart: { text: 'and then what', cursor: 4 },
wordMiddle: { text: 'and then what', cursor: 6 },
wordEnd: { text: 'and then what', cursor: 8 },
// cursor is after a sentence, before another
afterSentence: { text: 'Foo. Bar.', cursor: 5 },
afterQuestion: { text: 'Foo? Bar.', cursor: 5 },
afterExclamation: { text: 'Foo! Bar.', cursor: 5 }
};
// Utility function
var capitalizeWord = function capitalizeWord(word) {
if (word.length === 0) {
return word;
}
return word[0].toUpperCase() + word.substring(1);
};
// Test all the permutations of states above against these inputs.
// The property name is the input. The property value is a function
// that returns the expected output
// Does space punc get transposed to punc space?
var expectedPunctuation =
function expectedPunctuation(input, type, mode, value, cursor) {
// Don't run all permutations of this test for all inputs.
if (input[1] !== '.' && (type !== 'textarea' || mode !== 'latin-prose')) {
return;
}
// if the type is wrong, do nothing
if (type !== 'text' && type !== 'textarea' && type !== 'search') {
return input;
}
// if the mode is wrong do nothing
if (mode === 'verbatim' || mode === 'latin') {
return input;
}
// If mode is not specified, and we're not a text area, that is the
// same as latin mode, so do nothing
if (!mode && type !== 'textarea') {
return input;
}
// If input is a space followed by a colon or semicolon, do not transpose.
// This facilitates the entry of emoticons such as :O
if (input === ' :' || input === ' ;') {
return input;
}
// If the previous character is a letter, transpose otherwise don't
if (cursor > 0 && /[a-zA-Z]/.test(value.charAt(cursor - 1))) {
return input[1] + input[0];
}
return input;
};
var expectedSpaceSpace =
function expectedSpaceSpace(input, type, mode, value, cursor) {
// if the type is wrong, do nothing
if (type !== 'text' && type !== 'textarea' && type !== 'search') {
return input;
}
// if the mode is wrong do nothing
if (mode === 'verbatim' || mode === 'latin') {
return input;
}
// If mode is not specified, and we're not a text area, that is the
// same as latin mode, so do nothing
if (!mode && type !== 'textarea') {
return input;
}
// If the previous character is a letter, return dot space
if (cursor > 0 && /[a-zA-Z]/.test(value[cursor - 1])) {
return '. ';
}
return ' ';
};
var expectedCapitalization =
function expectedCapitalization(input, type, mode, value, cursor) {
// if the type is wrong, do nothing
if (type !== 'text' && type !== 'textarea' && type !== 'search') {
return input;
}
// if the mode is wrong do nothing
if (mode === 'verbatim' || mode === 'latin') {
return input;
}
// If mode is not specified, and we're not a text area, that is the
// same as latin mode, so do nothing
if (!mode && type !== 'textarea') {
return input;
}
// If we're still here, we're in latin-prose mode, and we may need
// to capitalize, depending on the value and cursor position.
if (cursor === 0) {
return capitalizeWord(input);
}
// if the character before the cursor is not a space, don't capitalize
if (!/\s/.test(value[cursor - 1])) {
return input;
}
// If we're at then end of a sentence, capitalize
if (/[.?!]\s+$/.test(value.substring(0, cursor))) {
return capitalizeWord(input);
}
// Otherwise, just return the input
return input;
};
var inputs = {
'ab': expectedCapitalization, // Does it get capitalized?
' ': expectedSpaceSpace, // Does it turn into ". "?
' .': expectedPunctuation, // Does it get transposed?
' !': expectedPunctuation, // Does it get transposed?
' ?': expectedPunctuation, // Does it get transposed?
' ,': expectedPunctuation, // Does it get transposed?
' ;': expectedPunctuation, // Does it get transposed?
' :': expectedPunctuation // Does it get transposed?
};
var constructTest =
function constructTest(input, type, mode, statename) {
var modeTitle = '-' + (mode ? mode : 'default');
var testname = type + modeTitle + '-' + statename +
'-' + input;
var state = contentStates[statename];
var expected =
inputs[input](input, type, mode, state.text, state.cursor);
// Skip the test if the expected function returns nothing.
// This is so we don't have too large a number of tests.
if (expected === undefined) {
return;
}
test(testname, function(done) {
engine.activate('en', {
type: type,
inputmode: mode,
text: state.text,
selectionStart: state.cursor,
selectionEnd: state.cursor
},{ suggest: false, correct: false });
// Wait for getData() to resolve.
Promise.resolve().then(function() {
var promise = Promise.resolve();
input.split('').forEach(function(c) {
promise = promise.then(function() {
if (glue.mIsUpperCase) {
c = c.toUpperCase();
}
return engine.click(c.charCodeAt(0));
});
});
return promise.then(function() {
assert.equal(
glue.mOutput, expected,
'expected "' + expected + '" for input "' + input + '"');
});
}).then(done, done);
});
};
// For each test, we activate() the IM with a given initial state,
// then send it some input, and check the output.
// The initial state includes language, whether suggestions are enabled,
// input type, input mode, input value, cursor position
// (or selectionstart, selection end).
// There are lots of possible initial states, and we may have different
// output in each case.
teardown(function() {
engine.deactivate();
});
suite('Input keys one by one', function() {
for (var t = 0; t < types.length; t++) {
var type = types[t];
for (var m = 0; m < modes.length; m++) {
var mode = modes[m];
for (var statename in contentStates) {
for (var input in inputs) {
constructTest(input, type, mode, statename);
}
}
}
}
});
});
suite('Suggestions', function() {
// For most tests here we ignore user dictionary predictions (i.e. testing
// old behaviors). We explicitly test it as noted.
var activateEngineWithState =
function activateEngineWithState(value, cursorStart, cursorEnd) {
engine.activate('en', {
type: 'text',
inputmode: 'latin-prose',
text: value,
selectionStart: cursorStart || value.length,
selectionEnd: cursorEnd || value.length
}, {
suggest: true,
correct: true
});
// Must wait for getData() to resolve in these tests.
return Promise.resolve();
};
var activateAndTestPrediction =
function activateAndTestPrediction(value, input, suggestions) {
return activateEngineWithState(value).then(function() {
workerStub.onmessage({
data: {
cmd: 'predictions',
input: input, // old input
suggestions: suggestions
}
});
});
};
// for most tests we deliberately ignore all user-dictionary mechanisms to
// ease testing of flow, by directly intercepting the last Promise.all call
setup(function() {
// the empty {} was what we'd resolved into for built-in dictionary (see
// getData above for glue stub)
this.sinon.stub(Promise, 'all').returns(Promise.resolve([{}]));
});
teardown(function() {
Promise.all.restore();
engine.deactivate();
});
suite('Handling user dictionary blob', function() {
test('correctly passed to worker through setLanguageSync',
function(done) {
// test strategy is a bit complex: we want to test that the blob from
// PromiseStorage is propogated to the Promise.all call and we want to
// know the setLanguage call used results from Promise.all call.
// The former is tested by then()'ing the Promise.all argument, and the
// latter by asserting worker postMessage stub. However, since the
// former one will happen in the next tick, we'll also need to wrap
// ourselves in Promise.all. So proper restoration on Promise.all is
// very important.
var resolvePromiseStorageBlob, rejectPromiseStorageBlob;
var resolveWorkerPostMessage;
var pPromiseStorageBlob = new Promise(function(resolve, reject) {
resolvePromiseStorageBlob = resolve;
rejectPromiseStorageBlob = reject;
});
var pWorkerPostMessage = new Promise(function(resolve) {
resolveWorkerPostMessage = resolve;
resolve();
});
Promise.all.restore();
var pAll = Promise.all([pPromiseStorageBlob, pWorkerPostMessage]);
this.sinon.stub(Promise, 'all')
.returns(Promise.resolve([{}, {user: 'user'}]));
activateEngineWithState('').then(function(){
Promise.all.firstCall.args[0][1].then(blob => {
assert.deepEqual(blob, {user: 'user'});
resolvePromiseStorageBlob();
}).catch(e => {
rejectPromiseStorageBlob(e);
});
sinon.assert.calledWith(
workerStub.postMessage,
{ args: ['en', {}, {user: 'user'}], cmd: 'setLanguage' },
[{}, {user: 'user'}]);
resolveWorkerPostMessage();
});
// calling then(done) (the first parameter) will make done receive an
// array of Promise resolutions and trigger mocha fail, so let's wrap it
pAll.then(() => {
done();
}, done);
});
test('call setUserDictionary on activation', function(done) {
// activate the engine twice to make sure worker is there at the second
// time.
activateEngineWithState('').then(function(){
activateEngineWithState('').then(function(){
sinon.assert.calledWith(
workerStub.postMessage,
{ args: [{user: 'user'}], cmd: 'setUserDictionary'},
[{user: 'user'}]);
done();
});
});
});
});
suite('Without handling user dictionary', function() {
test('After activation (and do nothing)', function(done) {
activateEngineWithState('').then(function() {
// maybe we shouldnt call this at all? don't know...
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(glue.sendCandidates, []);
// Also, a space should not be inserted
sinon.assert.callCount(glue.sendKey, 0);
}).then(done, done);
});
test('Suggestion data doesnt match input? Ignore.', function(done) {
activateAndTestPrediction('janj', 'jan', [
['Jan', 1],
['jan', 1],
['Pietje', 1]
]).then(function() {
// maybe we shouldnt call this at all? don't know...
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(glue.sendCandidates, []);
}).then(done, done);
});
test('One char input "n" should not autocorrect to a multichar word',
function(done) {
activateAndTestPrediction('n', 'n', [
['no', 1], // we want to ensure that this first suggestion is not
// marked (with * prefix) as an autocorrection
['not', 1],
['now', 1]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// maybe we shouldnt call this at all? don't know...
sinon.assert.calledWith(glue.sendCandidates,
['no', 'not', 'now']); // Make sure we do not get "*no"
}).then(done, done);
});
test('One char input "i" should autocorrect to a multichar word',
function(done) {
// But we also want to be sure that single letters like i do get
// autocorrected to single letter words like I
activateAndTestPrediction('i', 'i', [
['I', 1], // we want to ensure that this first suggestion is not
// marked (with * prefix) as an autocorrection
['in', 1],
['it', 1]
]).then(function() {
sinon.assert.calledWith(
glue.sendCandidates, ['*I', 'in', 'it']);
}).then(done, done);
});
test('Input "im" should autocorrect to "I\'m", not "in"',
function(done) {
activateAndTestPrediction('im', 'im', [
['in', 21],
['I\'m', 16],
['km', 9],
['um', 9]
]).then(function() {
sinon.assert.calledWith(
glue.sendCandidates, ['*I\'m', 'in', 'km']);
}).then(done, done);
});
test('Input "id" should autocorrect to "I\'d", not "is"',
function(done) {
activateAndTestPrediction('id', 'id', [
['is', 20],
['I\'d', 19],
['if', 17],
['ID', 16]
]).then(function() {
sinon.assert.calledWith(
glue.sendCandidates, ['*I\'d', 'ID', 'is']);
}).then(done, done);
});
test('Space to accept suggestion', function(done) {
activateAndTestPrediction('jan', 'jan', [
['Jan'],
['han'],
['Pietje']
]).then(function() {
return engine.click(KeyboardEvent.DOM_VK_SPACE).then(function() {
sinon.assert.callCount(glue.replaceSurroundingText, 1);
sinon.assert.calledWith(glue.replaceSurroundingText, 'Jan', -3, 3);
sinon.assert.calledWith(glue.sendKey, KeyboardEvent.DOM_VK_SPACE);
});
}).then(done, done);
});
test('Should communicate updated text to worker', function(done) {
function clickAndAssert(key, assertion) {
return engine.click(key.charCodeAt(0)).then(function() {
sinon.assert.calledWith(workerStub.postMessage,
{ args: [assertion], cmd: 'predict' });
});
}
activateEngineWithState('').then(function() {
return clickAndAssert('p', 'p');
}).then(function() {
return clickAndAssert('a', 'pa');
}).then(function() {
return clickAndAssert('i', 'pai');
}).then(function() {
// 4 because we have one extra setUserDictionary call
sinon.assert.callCount(workerStub.postMessage, 4);
}).then(done, done);
});
test('Two spaces after suggestion should autopunctuate', function(done) {
activateAndTestPrediction('jan', 'jan', [
['Jan'],
['han'],
['Pietje']
]).then(function() {
return engine.click(KeyboardEvent.DOM_VK_SPACE);
}).then(function() {
return engine.click(KeyboardEvent.DOM_VK_SPACE);
}).then(function() {
sinon.assert.callCount(glue.replaceSurroundingText, 1);
sinon.assert.calledWith(glue.replaceSurroundingText, 'Jan', -3, 3);
sinon.assert.callCount(glue.sendKey, 4);
assert.equal(glue.sendKey.args[0][0], KeyboardEvent.DOM_VK_SPACE);
assert.equal(glue.sendKey.args[1][0],
KeyboardEvent.DOM_VK_BACK_SPACE);
assert.equal(glue.sendKey.args[2][0], '.'.charCodeAt(0));
assert.equal(glue.sendKey.args[3][0], ' '.charCodeAt(0));
}).then(done, done);
});
test('New line then dot should not remove newline', function(done) {
activateEngineWithState('Hello').then(function() {
return engine.click(KeyboardEvent.DOM_VK_RETURN);
}).then(function() {
return engine.click('.'.charCodeAt(0));
}).then(function() {
sinon.assert.callCount(glue.replaceSurroundingText, 0);
sinon.assert.callCount(glue.sendKey, 2);
assert.equal(glue.sendKey.args[0][0], KeyboardEvent.DOM_VK_RETURN);
assert.equal(glue.sendKey.args[1][0], '.'.charCodeAt(0));
}).then(done, done);
});
test('dismissSuggestions hides suggestions', function(done) {
activateEngineWithState('').then(function() {
engine.dismissSuggestions();
// Send candidates should be called once with an empty array
// to clear the list of word suggestions
sinon.assert.callCount(glue.sendCandidates, 2);
sinon.assert.calledWith(glue.sendCandidates, []);
// Also, a space should not be inserted
sinon.assert.callCount(glue.sendKey, 0);
}).then(done, done);
});
});
suite('Uppercase suggestions', function(done) {
test('All uppercase input yields uppercase suggestions', function() {
activateAndTestPrediction('HOLO', 'HOLO', [
['yolo', 10],
['Yelp', 5],
['whuuu', 4]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// Verify that we show 3 suggestions that do not include the input
// and that we do not mark the first as an autocorrection.
sinon.assert.calledWith(
glue.sendCandidates, ['*YOLO', 'YELP', 'WHUUU']);
}).then(done, done);
});
test('One char uppercase not yields uppercase suggestions',
function(done) {
activateAndTestPrediction('F', 'F', [
['yolo', 10],
['Yelp', 5],
['whuuu', 4]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// Verify that we show 3 suggestions that do not include the input
// and that we do not mark the first as an autocorrection.
sinon.assert.calledWith(
glue.sendCandidates, ['yolo', 'Yelp', 'whuuu']);
}).then(done, done);
});
});
suite('handleSuggestions', function() {
test('input is not a word', function(done) {
activateAndTestPrediction('jan', 'jan', [
['Jan', 1],
['han', 1],
['Pietje', 1],
['extra', 1]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// Show 3 suggestions and mark the first as an autocorrect
sinon.assert.calledWith(glue.sendCandidates,
['*Jan', 'han', 'Pietje']);
}).then(done, done);
});
test('input is a common word', function(done) {
activateAndTestPrediction('the', 'the', [
['the', 10],
['they', 5],
['then', 4],
['there', 3]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// Verify that we show 3 suggestions that do not include the input
// and that we do not mark the first as an autocorrection.
sinon.assert.calledWith(glue.sendCandidates,
['they', 'then', 'there']);
}).then(done, done);
});
test('input is an uncommon word', function(done) {
activateAndTestPrediction('wont', 'wont', [
['won\'t', 11],
['wont', 8],
['won', 7],
['went', 6]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
// Verify that we show 3 suggestions that do not include the input
// and that we do mark the first as an autocorrection because it is
// more common than the valid word input.
sinon.assert.calledWith(glue.sendCandidates,
['*won\'t', 'won', 'went']);
}).then(done, done);
});
test('Foe', function(done) {
activateAndTestPrediction('foe', 'foe', [
['for', 16.878906249999996],
['foe', 15],
['Doe', 7.566406249999998],
['doe', 6.984374999999998]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(glue.sendCandidates,
['for', 'Doe', 'doe']);
}).then(done, done);
});
test('Hid', function(done) {
activateAndTestPrediction('hid', 'hid', [
['his', 16.296874999999996],
['hid', 16],
['HUD', 7.415834765624998],
['hide', 7.2]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['his', 'HUD', 'hide']);
}).then(done, done);
});
suite('Suggestion length mismatch', function(done) {
test('Length mismatch, low freq', function(done) {
activateAndTestPrediction('zoolgy', 'zoolgy', [
['zoology', 4.2],
['Zoology\'s', 0.09504000000000001]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['zoology', 'Zoology\'s']);
}).then(done, done);
});
test('Length mismatch, medium freq', function(done) {
activateAndTestPrediction('Folow', 'Folow', [
['Follow', 6.237],
['Follows', 2.4948],
['Followed', 1.0454400000000001],
['Follower', 0.7603200000000001]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['*Follow', 'Follows', 'Followed']);
}).then(done, done);
});
test('Length mismatch, high freq', function(done) {
activateAndTestPrediction('awesomeo', 'awesomeo', [
['awesome', 31],
['trahlah', 8],
['moarstu', 7]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['*awesome', 'trahlah', 'moarstu']);
}).then(done, done);
});
});
suite('Always keep at least one user dictionary word', function() {
test('User dictionary word frequency is high', function(done){
activateAndTestPrediction('ster', 'ster', [
['stery', 4],
['star', 3, true],
['stak', 2],
['stack', 1]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['stery', 'star', 'stak']);
}).then(done, done);
});
suite('User dictionary word frequency is low but not too low',
function(){
var testWithLastUDSuggestionFreq = function(freq, done) {
activateAndTestPrediction('ster', 'ster', [
['stery', 4],
['star', 3],
['stak', 2],
['stack', freq, true]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['stery', 'star', 'stack']);
}).then(done, done);
};
test('UD word freq = 1.5', function(done){
testWithLastUDSuggestionFreq(1.5, done);
});
test('UD word req = 1', function(done){
testWithLastUDSuggestionFreq(1, done);
});
});
suite('User dictionary word frequency is too low',
function(){
var testWithLastUDSuggestionFreq = function(freq, done) {
activateAndTestPrediction('ster', 'ster', [
['stery', 4],
['star', 3],
['stak', 2],
['stack', freq, true]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['stery', 'star', 'stak']);
}).then(done, done);
};
test('UD word freq = 0.99', function(done){
testWithLastUDSuggestionFreq(0.99, done);
});
test('UD word req = 0.1', function(done){
testWithLastUDSuggestionFreq(0.1, done);
});
test('UD word req = 0.01', function(done){
testWithLastUDSuggestionFreq(0.01, done);
});
});
test('A frequent user dictionary word is input', function(done){
activateAndTestPrediction('ster', 'ster', [
['ster', 10, true],
['star', 3],
['stak', 2],
['stack', 1]
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['star', 'stak', 'stack']);
}).then(done, done);
});
test('User dictionary word frequency is too low but still higher' +
' than next built-in dictionary\'s frequency',
function(done){
activateAndTestPrediction('ster', 'ster', [
['stery', 4],
['star', 3],
['stack', 0.99, true],
['stak', 0.95],
]).then(function() {
sinon.assert.callCount(glue.sendCandidates, 1);
sinon.assert.calledWith(
glue.sendCandidates, ['stery', 'star', 'stack']);
}).then(done, done);
});
});
});
});
suite('Reject one of the keys', function() {
setup(function(done) {
engine.activate('en', {
type: 'text',
inputmode: '',
text: '',
selectionStart: 0,
selectionEnd: 0
},{ suggest: false, correct: false });
// Wait for getData() to resolve.
Promise.resolve().then(done);
});
teardown(function() {
engine.deactivate();
});
test('reject and resolve another', function(done) {
glue.mResolveSendKey = false;
engine
.click('a'.charCodeAt(0))
.then(function() {
glue.mResolveSendKey = true;
return engine.click('b'.charCodeAt(0));
})
.then(function() {
assert.equal('b', glue.mOutput);
})
.catch(function(e) {
throw (e || 'Should not reject.');
})
.then(done, done);
});
});
suite('stateChange', function() {
setup(function(done) {
engine.activate('en', {
type: 'text',
inputmode: '',
text: 'before after',
selectionStart: 5,
selectionEnd: 5
}, { suggest: true, correct: true });
// Wait for getData() to resolve.
Promise.resolve().then(done);
});
teardown(function() {
engine.deactivate();
});
test('will clear the suggestions if stateChange', function() {
engine.stateChange({
ownAction: false,
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'before after'
});
// will clear the suggestions since cursor changed
sinon.assert.calledThrice(glue.sendCandidates);
});
test('Do nothing if stateChange called w/o changes', function() {
engine.stateChange({
ownAction: true,
type: 'text',
inputmode: '',
text: 'before after',
selectionStart: 5,
selectionEnd: 5
});
// will clear the suggestions since cursor changed
sinon.assert.calledOnce(glue.sendCandidates);
});
});
suite('Layout handling', function() {
setup(function() {
engine.activate('en', {
type: 'text',
inputmode: '',
text: '',
selectionStart: 0,
selectionEnd: 0
},{ suggest: false, correct: false });
});
teardown(function() {
engine.deactivate();
});
test('Do not switch layout if current layout is default', function(done) {
engine.click(KeyEvent.DOM_VK_RETURN)
.then(() => sinon.assert.notCalled(glue.setLayoutPage))
.then(done, done);
});
test('Do not switch layout if current layout is default', function(done) {
engine.setLayoutPage(1);
engine.setLayoutPage(PAGE_INDEX_DEFAULT);
engine.click(KeyEvent.DOM_VK_RETURN)
.then(() => sinon.assert.notCalled(glue.setLayoutPage))
.then(done, done);
});
test('Do not switch layout if a normal key was not clicked before',
function(done) {
engine.setLayoutPage(1);
engine.click(KeyEvent.DOM_VK_SPACE)
.then(() => sinon.assert.notCalled(glue.setLayoutPage))
.then(done, done);
});
test('Switch layout if a normal key then space clicked', function(done) {
engine.setLayoutPage(1);
engine.click('1'.charCodeAt(0))
.then(() => engine.click(KeyEvent.DOM_VK_SPACE))
.then(
() => sinon.assert.calledWith(glue.setLayoutPage, PAGE_INDEX_DEFAULT)
)
.then(done, done);
});
test('Switch layout if a normal key is clicked on one non-default layout, '+
'then space clicked on another non-default layout', function(done) {
engine.setLayoutPage(1);
engine.click('1'.charCodeAt(0))
.then(() => {
engine.setLayoutPage(2);
return engine.click(KeyEvent.DOM_VK_SPACE);
})
.then(
() => sinon.assert.calledWith(glue.setLayoutPage, PAGE_INDEX_DEFAULT)
)
.then(done, done);
});
});
});

View file

@ -0,0 +1,875 @@
/*global InputMethodDatabaseLoader, WordListConverter, InputMethods, dump */
'use strict';
require('/js/settings/word_list_converter.js');
require('/js/keyboard/input_method_database_loader.js');
suite('Latin worker', function() {
var worker;
var keymaps = {};
suiteSetup(function(done) {
window.InputMethods = {};
require('/js/imes/latin/latin.js', function() {
keymaps.qwerty = InputMethods.latin.generateNearbyKeyMap({
'keyboardWidth': 320,
'keyboardHeight': 205,
'keyArray': [{
'code': 113,
'x': 0,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 119,
'x': 32,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 101,
'x': 64,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 114,
'x': 96,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 116,
'x': 128,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 121,
'x': 160,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 117,
'x': 192,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 105,
'x': 224,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 111,
'x': 256,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 112,
'x': 288,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 97,
'x': 16,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 115,
'x': 48,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 100,
'x': 80,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 102,
'x': 112,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 103,
'x': 144,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 104,
'x': 176,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 106,
'x': 208,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 107,
'x': 240,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 108,
'x': 272,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 20,
'x': 0,
'y': 131,
'width': 48,
'height': 43
}, {
'code': 122,
'x': 48,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 120,
'x': 80,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 99,
'x': 112,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 118,
'x': 144,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 98,
'x': 176,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 110,
'x': 208,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 109,
'x': 240,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 8,
'x': 272,
'y': 131,
'width': 48,
'height': 43
}, {
'code': 65534,
'x': 0,
'y': 182,
'width': 48,
'height': 43
}, {
'code': 65533,
'x': 48,
'y': 182,
'width': 32,
'height': 43
}, {
'code': 32,
'x': 80,
'y': 182,
'width': 144,
'height': 43
}, {
'code': 46,
'x': 224,
'y': 182,
'width': 32,
'height': 43
}, {
'code': 13,
'x': 256,
'y': 182,
'width': 64,
'height': 43
}],
'keyWidth': 32,
'keyHeight': 50
});
keymaps.azerty = InputMethods.latin.generateNearbyKeyMap({
'keyboardWidth': 320,
'keyboardHeight': 205,
'keyArray': [{
'code': 97,
'x': 0,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 122,
'x': 32,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 101,
'x': 64,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 114,
'x': 96,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 116,
'x': 128,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 121,
'x': 160,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 117,
'x': 192,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 105,
'x': 224,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 111,
'x': 256,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 112,
'x': 288,
'y': 29,
'width': 32,
'height': 43
}, {
'code': 113,
'x': 0,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 115,
'x': 32,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 100,
'x': 64,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 102,
'x': 96,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 103,
'x': 128,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 104,
'x': 160,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 106,
'x': 192,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 107,
'x': 224,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 108,
'x': 256,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 109,
'x': 288,
'y': 80,
'width': 32,
'height': 43
}, {
'code': 20,
'x': 0,
'y': 131,
'width': 48,
'height': 43
}, {
'code': 119,
'x': 48,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 120,
'x': 80,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 99,
'x': 112,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 118,
'x': 144,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 98,
'x': 176,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 110,
'x': 208,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 39,
'x': 240,
'y': 131,
'width': 32,
'height': 43
}, {
'code': 8,
'x': 272,
'y': 131,
'width': 48,
'height': 43
}, {
'code': 18,
'x': 0,
'y': 182,
'width': 64,
'height': 43
}, {
'code': -3,
'x': 64,
'y': 182,
'width': 32,
'height': 43
}, {
'code': 32,
'x': 96,
'y': 182,
'width': 128,
'height': 43
}, {
'code': 46,
'x': 224,
'y': 182,
'width': 32,
'height': 43
}, {
'code': 13,
'x': 256,
'y': 182,
'width': 64,
'height': 43
}],
'keyWidth': 32,
'keyHeight': 51
});
done();
});
});
setup(function() {
worker = new Worker('../../../../js/imes/latin/worker.js');
});
teardown(function() {
worker.terminate();
worker.onmessage = null;
worker = null;
});
function onWorkerMessage(filter) {
return new Promise(function(resolve,reject) {
worker.onmessage = function handleWorkerMessage(evt) {
if (filter !== undefined && evt.data.cmd !== filter) {
return;
}
worker.onmessage = null;
resolve(evt.data);
};
});
}
function prediction(input, expected) {
worker.postMessage({cmd: 'predict', args: [input]});
return onWorkerMessage('predictions').then(function(data) {
var suggestions = data.suggestions;
assert.equal(data.cmd, 'predictions');
assert.equal(data.input, input);
suggestions = suggestions.map(function(s) {
return s[0];
});
assert.equal(suggestions[0], expected[0]);
for (var i = 1; i < 6; i++) {
if (expected[i] !== null) {
assert.equal(suggestions[i], expected[i], 'index: ' + i);
}
}
});
}
function setupLanguage(langCode, keymap, userDictBlob) {
var loader = new InputMethodDatabaseLoader();
loader.start();
loader.SOURCE_DIR = '/js/imes/';
var loaderPromise =
loader.load('latin', 'dictionaries/' + langCode + '.dict')
.then(function(dictData) {
worker.postMessage({
cmd: 'setLanguage',
args: [langCode, dictData, userDictBlob]
}, [dictData]);
});
worker.postMessage({
cmd: 'setNearbyKeys',
args: [keymap]
});
var successCount = 0;
var waitForSuccessPromise = new Promise(function(resolve, reject) {
worker.onmessage = function(e) {
if (e.data.cmd !== 'success') {
reject('worker.onmessage unexpected result ' + e.data.message + '\n');
}
if (e.data.fn.startsWith('setLanguage') ||
e.data.fn.startsWith('setNearbyKeys')) {
successCount++;
if (successCount === 4) {
worker.onmessage = null;
resolve();
}
}
};
});
return loaderPromise
.then(function() {
return waitForSuccessPromise;
});
}
test('Worker should throw if no dictData is passed', function(done) {
worker.postMessage({ cmd: 'setLanguage', args: ['en_us', null] });
onWorkerMessage('error').then(function(data){
assert.equal(data.cmd, 'error');
}).then(done, done);
});
suite('en_us predictions', function() {
setup(function(done) {
setupLanguage('en_us', keymaps.qwerty, undefined
).then(done,done);
});
test('i should be predicted as I', function(done) {
prediction('i', ['I', 'in', 'is', null, null, null]
).then(done,done);
});
test('Capital input should give capital output', function(done) {
prediction('City', ['City', 'City\'s', 'Fit', null, null, null]
).then(done, done);
});
test('Non-Capital input should give non-capital output', function(done) {
prediction('city', ['city', 'city\'s', 'fit', null, null, null]
).then(done, done);
});
test('Non existing word should not be matched', function(done) {
prediction('sadjasuufehwuefhwejfd', [undefined, null, null, null]
).then(done, done);
});
test('$ should not yield autosuggest', function(done) {
prediction('$', [undefined, null, null, null]
).then(done, done);
});
suite('Capitalization and suggestions', function() {
test('virgule', function(done) {
prediction('virgule',
['virgule', 'virgules', 'Virgil', null, null, null]
).then(done, done);
});
test('Virgule', function(done) {
prediction('Virgule',
['Virgule', 'Virgules', 'Virgil', null, null, null]
).then(done, done);
});
test('virgul', function(done) {
prediction('virgul',
['Virgil', 'Virgil\'s', 'virgule', null, null, null]
).then(done, done);
});
test('Virgul', function(done) {
prediction('Virgul',
['Virgil', 'Virgil\'s', 'Virgule', null, null, null]
).then(done, done);
});
test('balds', function(done) {
prediction('balds',
['balds', 'Baldwin', 'Baldwins', null, null, null]
).then(done, done);
});
test('Balds', function(done) {
prediction('Balds',
['Balds', 'Baldwin', 'Baldwins', null, null, null]
).then(done, done);
});
test('chaot', function(done) {
prediction('chaot',
['chaotic', 'chapter', 'chapters', null, null, null]
).then(done, done);
});
test('Chaot', function(done) {
prediction('Chaot',
['Chaotic', 'Chapter', 'Chapters', null, null, null]
).then(done, done);
});
test('as', function(done) {
prediction('as', ['as', 'ad', 'AD', null, null, null]
).then(done, done);
});
test('As', function(done) {
prediction('As', ['As', 'Ad', 'AD', null, null, null]
).then(done, done);
});
test('keyboa', function(done) {
prediction('keyboa', ['keyboard', null, null, null, null, null]
).then(done, done);
});
});
suite('Low frequency dictionary words with better suggestion', function() {
test('wont', function(done) {
prediction('wont', ['won\'t', 'wont', 'Wong', null, null, null]
).then(done, done);
});
test('cant', function(done) {
prediction('cant', ['can\'t', 'cant', 'canto', null, null, null]
).then(done, done);
});
});
suite('Swear words', function() {
test('Should not suggest word in offensive list #1', function(done) {
worker.postMessage({ cmd: 'predict', args: ['fuc'] });
onWorkerMessage().then(function(data) {
var suggestions = data.suggestions;
assert.notEqual(suggestions[0][0], 'fuck');
assert.notEqual(suggestions[1][0], 'fuck');
assert.notEqual(suggestions[2][0], 'fuck');
}).then(done, done);
});
test('Should not suggest word in offensive list #2', function(done) {
worker.postMessage({ cmd: 'predict', args: ['peni'] });
onWorkerMessage().then(function(data) {
var suggestions = data.suggestions;
assert.notEqual(suggestions[0][0], 'penis');
assert.notEqual(suggestions[1][0], 'penis');
assert.notEqual(suggestions[2][0], 'penis');
}).then(done, done);
});
test('Should not suggest word in offensive list #3', function(done) {
worker.postMessage({ cmd: 'predict', args: ['fuck'] });
onWorkerMessage().then(function(data) {
var suggestions = data.suggestions;
assert.equal(suggestions[1][0], 'duck');
assert.equal(suggestions[2][0], 'Tuck');
}).then(done, done);
});
test('Should suggest offensive word if matches input #1', function(done) {
prediction('fuck', ['fuck', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #2', function(done) {
prediction('penis', ['penis', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #3', function(done) {
prediction('Penis', ['Penis', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #4', function(done) {
prediction('Vagina', ['Vagina', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #5', function(done) {
prediction('Fuck', ['Fuck', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #6', function(done) {
prediction('shit', ['shit', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #7', function(done) {
prediction('prick', ['prick', null, null, null, null, null]
).then(done, done);
});
test('Should suggest offensive word if matches input #7', function(done) {
prediction('ass', ['ass', null, null, null, null, null]
).then(done, done);
});
});
suite('Vertical nearby keys', function() {
test('Asjan / Asian', function(done) {
prediction('Asjan', ['Asian', null, null, null, null, null]
).then(done, done);
});
test('flr / for', function(done) {
prediction('flr', ['for', null, null, null, null, null]
).then(done, done);
});
test('kn / km / in / on', function(done) {
prediction('kn', ['km', 'in', 'on', null, null, null]
).then(done, done);
});
});
});
// a bit "cheating" here: if we use en_us in this suite then setLanguage will
// early-return at setting built-in dict and "success" count will only be 3.
// to avoid changing too many assertion logics we'll use en_gb and en_us
// alternately.
suite('predictions in conjuction with user dictionary', function() {
var blob;
setup(function() {
blob = new WordListConverter(
['Mozilla', 'MozSpace', 'MozTrap', 'mozSettings', 'Mozillian'])
.toBlob();
});
suite('with user dictionary blob at setupLanguage', function() {
setup(function(done) {
setupLanguage('en_gb', keymaps.qwerty, blob
).then(done,done);
});
test('Moz', function(done) {
prediction('Moz',
['Mox', 'Mos', 'Most', 'Mod', 'MozTrap', 'Mozilla']
).then(done, done);
});
test('mozs', function(done) {
prediction('mozs',
['moss', 'mods', 'mossy', 'Mossi', 'MozSpace', 'MozTrap']
).then(done, done);
});
});
suite('with user dictionary blob supplied later', function() {
setup(function(done) {
setupLanguage('en_us', keymaps.qwerty, undefined).then(function() {
worker.addEventListener('message', function onMessage(e) {
if ('success' === e.data.cmd && 'setUserDictionary' === e.data.fn) {
worker.removeEventListener('messgae', onMessage);
}
});
worker.postMessage({
cmd: 'setUserDictionary',
args: [blob]
});
}).then(done,done);
});
test('Moz', function(done) {
prediction('Moz',
['MOX', 'Mos', 'Most', 'Mod', 'MozTrap', 'Mozilla']
).then(done, done);
});
test('mozs', function(done) {
prediction('mozs',
['moss', 'Moss', 'mods', 'mossy', 'MozSpace', 'MozTrap']
).then(done, done);
});
});
suite('with user dictionary blob nullified later', function() {
setup(function(done) {
setupLanguage('en_gb', keymaps.qwerty, blob).then(function() {
worker.addEventListener('message', function onMessage(e) {
if ('success' === e.data.cmd && 'setUserDictionary' === e.data.fn) {
worker.removeEventListener('messgae', onMessage);
}
});
worker.postMessage({
cmd: 'setUserDictionary',
args: [undefined]
});
}).then(done,done);
});
// do not append "null" there: we want to make sure the suggestions cut at
// the fourth element.
test('Moz', function(done) {
prediction('Moz', ['Mox', 'Mos', 'Most', 'Mod']
).then(done, done);
});
test('mozs', function(done) {
prediction('mozs', ['moss', 'mods', 'mossy', 'Mossi']
).then(done, done);
});
});
});
suite('fr predictions', function() {
setup(function(done) {
setupLanguage('fr', keymaps.azerty, undefined
).then(done,done);
});
test('123 should not yield prediction', function(done) {
prediction('123', [null, null, null, null, null, null]
).then(done, done);
});
});
suite('validChars() is tolerant on small dicts', function() {
setup(function(done) {
var dictData = new WordListConverter(['Ápple']).toBlob();
worker.postMessage({
cmd: 'setNearbyKeys',
args: [keymaps.qwerty]
});
worker.postMessage({
cmd: 'setLanguage',
args: ['en_us', dictData]
});
var successCount = 0;
worker.onmessage = function(e) {
if (e.data.cmd !== 'success') {
dump('worker.onmessage unexpected result ' + e.message + '\n');
}
assert.equal(e.data.cmd, 'success');
if (e.data.fn.startsWith('setLanguage') ||
e.data.fn.startsWith('setNearbyKeys')) {
successCount++;
if (successCount === 2) {
done();
}
}
};
});
test('app should yield prediction', function(done) {
prediction('app', ['Ápple', null, null]
).then(done, done);
});
});
});

View file

@ -0,0 +1,106 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
requireApp('keyboard/test/unit/setup_engine.js');
requireApp('keyboard/js/imes/myanmar/myanmar.js');
suite('myanmar.js', function() {
var engine;
var BACKSPACE = 8;
var outputString = '';
var glue = {
sendKey: function(key) {
outputString = outputString + String.fromCharCode(key);
},
sendString: sinon.stub(),
setUpperCase: sinon.stub(),
replaceSurroundingText: function(str, index, num) {
assert.equal(num+index, 0);
outputString = str;
},
getNumberOfCandidatesPerRow: function() {
return 0;
}
};
setup(function() {
outputString = '';
this.sinon.spy(glue, 'replaceSurroundingText');
});
teardown(function() {
glue.replaceSurroundingText.reset();
});
test('Init', function() {
assert.isDefined(window.InputMethods.myanmar);
engine = window.InputMethods.myanmar;
engine.init(glue);
});
test('Input VowelSE', function() {
outputString = '';
engine.click('\u1031'.charCodeAt(0));
assert.equal(outputString, '\u200B\u1031');
});
function runTest(rules, action) {
rules.forEach(function(rule, index) {
test('case ' + index, function() {
engine.surroundingtextChange({beforeString:rule.input, afterString:''});
action();
assert.ok(glue.replaceSurroundingText.called);
assert.equal(outputString, rule.result);
});
});
}
suite('Add rules', function() {
runTest([
{ input:'\u1025\u102E', result: '\u1026' },
{ input:'\u1025\u103A', result: '\u1009\u103A' },
{ input:'\u101E\u103C\u1031\u102C\u103A', result: '\u102A' }
], sinon.stub());
});
suite('Swap rules', function() {
runTest([
// p1 + p2 => p2 + p1
{ input:'\u103E\u103B', result: '\u103B\u103E'},
{ input:'\u103D\u103B', result: '\u103B\u103D'},
{ input:'\u1036\u102E', result: '\u102E\u1036'},
{ input:'\u102F\u102D', result: '\u102D\u102F'},
{ input:'\u103A\u1037', result: '\u1037\u103A'},
{ input:'\u1037\u102E', result: '\u102E\u1037'},
{ input:'\u1036\u1032', result: '\u1032\u1036'},
{ input:'\u1031\u103B', result: '\u103B\u1031'},
// p1 + p2 + p3 => p2 + p3 + p1
{ input:'\u1031\u1039\u1000', result: '\u1039\u1000\u1031'},
// p1 + p2 + p3 => p3 + p1 + p2
{ input:'\u103D\u1031\u103C', result: '\u103C\u103D\u1031'},
{ input:'\u103D\u103E\u103C', result: '\u103C\u103D\u103E'},
{ input:'\u103E\u1031\u103B', result: '\u103B\u103E\u1031'},
// p1 + p2 + p3 => p3 + p2
{ input:'\u200B\u1031\u1000', result: '\u1000\u1031'},
// p1 + p2 + p3 + p4 => p4 + p1 + p2 + p3
{ input:'\u103D\u103E\u1031\u103C', result: '\u103C\u103D\u103E\u1031'}
], sinon.stub());
});
suite('Delete rules', function() {
runTest([
{ input:'\u1039\u1019\u1031', result: '\u1031\u1039' },
{ input:'\u1019\u1031', result: '\u200B\u1031' },
{ input:'\u103B\u1031', result: '\u1031' },
{ input:'\u1039\u1019', result: '' },
{ input:'\u200B\u1031', result: '' },
], function() {
engine.click(BACKSPACE);
});
});
});

View file

@ -0,0 +1,257 @@
/* global InputMethods, KeyEvent */
'use strict';
require('/test/unit/setup_engine.js');
require('/js/imes/jshangul/jshangul.js');
var SPACE = KeyEvent.DOM_VK_SPACE;
var BACKSPACE = KeyEvent.DOM_VK_BACK_SPACE;
suite('jshangul', function() {
var im;
function dummy() {}
var testableKeyboardGlue = {
// For Test Methods
_ready: function() {
this.gul = undefined;
this.endCompositionCount = 0;
},
_status: function() {
return [this.gul, this.endCompositionCount];
},
// Keyboard APIs
resetUpperCase: dummy,
sendKey: function(keyCode) {
this.gul = String.fromCharCode(keyCode);
},
sendCandidates: dummy,
setComposition: function(gul) {
this.gul = gul;
},
endComposition: function() {
this.endCompositionCount ++;
},
setUpperCase: dummy,
setLayoutPage: dummy,
isCapitalized: dummy
};
suiteSetup(function() {
im = InputMethods.jshangul;
im.init(testableKeyboardGlue);
});
test('load', function() {
assert.isDefined(im);
});
suite('Hangul Composition Test', function() {
var cases = [
{
info: 'should return "ㄲㄸ" when type "ㄱㄱㄷㄷ"',
scenario: [
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ 'ㄱ', [ 'ㄲ', 0 ] ],
[ 'ㄷ', [ 'ㄷ', 1 ] ],
[ 'ㄷ', [ 'ㄸ', 1 ] ]
]
},
{
info: 'should return "각" when type "ㄱㅏㄱ"',
scenario: [
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ 'ㅏ', [ '가', 0 ] ],
[ 'ㄱ', [ '각', 0 ] ]
]
},
{
info: 'should return "냐" when type "ㄴㅑ"',
scenario: [
[ 'ㄴ', [ 'ㄴ', 0 ] ],
[ 'ㅑ', [ '냐', 0 ] ]
]
},
{
info: 'should return "ㅏㄱ" when type "ㅏㄱ"',
scenario: [
[ 'ㅏ', [ 'ㅏ', 0 ] ],
[ 'ㄱ', [ 'ㄱ', 1 ] ]
]
},
{
info: 'should return "ㅢㅣ" when type "ㅡㅣㅣ"' ,
scenario: [
[ 'ㅡ', [ 'ㅡ', 0 ] ],
[ 'ㅣ', [ 'ㅢ', 0 ] ],
[ 'ㅣ', [ 'ㅣ', 1 ] ]
]
},
{
info: 'should return "ㅏㅑㅓㅕㅗㅛ" when type "ㅏㅑㅓㅕㅗㅛ"' ,
scenario: [
[ 'ㅏ', [ 'ㅏ', 0 ] ],
[ 'ㅑ', [ 'ㅑ', 1 ] ],
[ 'ㅓ', [ 'ㅓ', 2 ] ],
[ 'ㅕ', [ 'ㅕ', 3 ] ],
[ 'ㅗ', [ 'ㅗ', 4 ] ],
[ 'ㅛ', [ 'ㅛ', 5 ] ]
]
},
{
info: 'should return 신장미 when type "ㅅㅣㄴㅈㅏㅇㅁㅣ"',
scenario: [
[ 'ㅅ', [ 'ㅅ', 0 ] ],
[ 'ㅣ', [ '시', 0 ] ],
[ 'ㄴ', [ '신', 0 ] ],
[ 'ㅈ', [ '싡', 0 ] ],
[ 'ㅏ', [ '자', 1 ] ],
[ 'ㅇ', [ '장', 1 ] ],
[ 'ㅁ', [ 'ㅁ', 2 ] ],
[ 'ㅣ', [ '미', 2 ] ]
]
},
{
info: 'should return 깨끗 when type "ㄱㄱㅐㄲㅡㅅ"',
scenario: [
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ 'ㄱ', [ 'ㄲ', 0 ] ],
[ 'ㅐ', [ '깨', 0 ] ],
[ 'ㄲ', [ '깪', 0 ] ],
[ 'ㅡ', [ '끄', 1 ] ],
[ 'ㅅ', [ '끗', 1 ] ]
]
},
{
info: 'should return 밥&미 when type "ㅂㅏㅂ&ㅁㅣ"',
scenario: [
[ 'ㅂ', [ 'ㅂ', 0 ] ],
[ 'ㅏ', [ '바', 0 ] ],
[ 'ㅂ', [ '밥', 0 ] ],
[ '&', [ '&', 1 ] ],
[ 'ㅁ', [ 'ㅁ', 1 ] ],
[ 'ㅣ', [ '미', 1 ] ]
]
},
{
info: 'should return 미&밥 when type "ㅁㅣ&ㅂㅏㅂ"',
scenario: [
[ 'ㅁ', [ 'ㅁ', 0 ] ],
[ 'ㅣ', [ '미', 0 ] ],
[ '&', [ '&', 1 ] ],
[ 'ㅂ', [ 'ㅂ', 1 ] ],
[ 'ㅏ', [ '바', 1 ] ],
[ 'ㅂ', [ '밥', 1 ] ]
]
},
{
info: 'should return 뷁 when type "ㅂㅜㅔㄹㄱ"',
scenario: [
[ 'ㅂ', [ 'ㅂ', 0 ] ],
[ 'ㅜ', [ '부', 0 ] ],
[ 'ㅔ', [ '붸', 0 ] ],
[ 'ㄹ', [ '뷀', 0 ] ],
[ 'ㄱ', [ '뷁', 0 ] ]
]
},
{
info: 'should return 한 글 when type "ㅎㅏㄴ[SPACE]ㄱㅡㄹ"',
scenario: [
[ 'ㅎ', [ 'ㅎ', 0 ] ],
[ 'ㅏ', [ '하', 0 ] ],
[ 'ㄴ', [ '한', 0 ] ],
[ SPACE ,[ ' ', 1 ] ],
[ 'ㄱ', [ 'ㄱ', 1 ] ],
[ 'ㅡ', [ '그', 1 ] ],
[ 'ㄹ', [ '글', 1 ] ]
]
},
{
info: 'should return ㄱㄴㄷㄹㅁㅂ when type "ㄱㄴㄷㄹㅁㅂ"',
scenario: [
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ 'ㄴ', [ 'ㄴ', 1 ] ],
[ 'ㄷ', [ 'ㄷ', 2 ] ],
[ 'ㄹ', [ 'ㄹ', 3 ] ],
[ 'ㅁ', [ 'ㅁ', 4 ] ]
]
},
{
info: 'should return 사랑 when type "ㅅㅏㄹㅏㅇ"',
scenario: [
[ 'ㅅ', [ 'ㅅ', 0 ] ],
[ 'ㅏ', [ '사', 0 ] ],
[ 'ㄹ', [ '살', 0 ] ],
[ 'ㅏ', [ '라', 1 ] ],
[ 'ㅇ', [ '랑', 1 ] ]
]
},
{
info: 'should return 장미 when type "ㅈㅏㅇㅁㅣ"',
scenario: [
[ 'ㅈ', [ 'ㅈ', 0 ] ],
[ 'ㅏ', [ '자', 0 ] ],
[ 'ㅇ', [ '장', 0 ] ],
[ 'ㅁ', [ 'ㅁ', 1 ] ],
[ 'ㅣ', [ '미', 1 ] ]
]
},
{
info: 'should return 어ㅠㅠ when type "ㅇㅓㅠㅠ"',
scenario: [
[ 'ㅇ', [ 'ㅇ', 0 ] ],
[ 'ㅓ', [ '어', 0 ] ],
[ 'ㅠ', [ 'ㅠ', 1 ] ],
[ 'ㅠ', [ 'ㅠ', 2 ] ]
]
},
{
info: 'should 깎 rightly write and delete by jamo.',
scenario: [
[ 'ㄱ', [ 'ㄱ' , 0 ] ],
[ 'ㄱ', [ 'ㄲ' , 0 ] ],
[ 'ㅏ', [ '까' , 0 ] ],
[ 'ㄲ', [ '깎' , 0 ] ],
[ BACKSPACE, [ '까', 0 ] ],
[ BACKSPACE, [ 'ㄲ', 0 ] ],
[ BACKSPACE, [ 'ㄱ', 0 ] ],
[ BACKSPACE, [ '', 0 ] ]
]
},
{
info: 'should ㄱ rightly write and delete 2 times.',
scenario: [
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ BACKSPACE, [ '', 0 ] ],
[ 'ㄱ', [ 'ㄱ', 0 ] ],
[ BACKSPACE, [ '', 0 ] ]
]
}
];
setup(function() {
im.deactivate();
testableKeyboardGlue._ready();
});
cases.forEach(function(theCase) {
var msg = theCase.info;
var clicksAndExpects = theCase.scenario;
test(msg, function() {
for(var i = 0; i < clicksAndExpects.length; i++) {
var clickAndThen = clicksAndExpects[i];
var input = clickAndThen[0];
var charCode = (typeof input === 'number') ?
input : input.charCodeAt(0);
im.click(charCode);
assert.deepEqual(testableKeyboardGlue._status(), clickAndThen[1]);
}
});
});
});
});

View file

@ -0,0 +1,92 @@
'use strict';
/* global AbortablePromiseQueue */
require('/js/keyboard/abortable_promise_queue.js');
suite('AbortablePromiseQueue', function() {
var queue;
setup(function() {
queue = new AbortablePromiseQueue();
queue.start();
});
teardown(function(done) {
queue.run([function() {
queue.stop();
done();
}]);
});
test('run()', function(done) {
var tasks = [
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub()
];
var p = queue.run(tasks);
p.then(function() {
tasks.forEach(function(stub, i) {
assert.isTrue(stub.calledOnce);
if (i !== 0) {
assert.isTrue(tasks[i - 1].calledBefore(stub));
}
});
}).then(done, done);
});
test('abort()', function(done) {
var tasks = [
this.sinon.stub(),
this.sinon.stub(),
function() {
queue.abort();
},
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub()
];
var p = queue.run(tasks);
p.then(function() {
assert.isTrue(tasks[0].calledOnce);
assert.isTrue(tasks[1].calledOnce);
assert.isFalse(tasks[3].calledOnce);
assert.isFalse(tasks[4].calledOnce);
assert.isFalse(tasks[5].calledOnce);
}).then(done, done);
});
test('run() another task', function(done) {
var tasks2 = [
this.sinon.stub()
];
var tasks = [
this.sinon.stub(),
this.sinon.stub(),
function() {
queue.run(tasks2).then(function() {
assert.isTrue(tasks[0].calledOnce);
assert.isTrue(tasks[1].calledOnce);
assert.isFalse(tasks[3].calledOnce);
assert.isFalse(tasks[4].calledOnce);
assert.isFalse(tasks[5].calledOnce);
assert.isTrue(tasks2[0].calledOnce);
assert.isTrue(tasks[1].calledBefore(tasks2[0]));
}).then(done, done);
},
this.sinon.stub(),
this.sinon.stub(),
this.sinon.stub()
];
queue.run(tasks);
});
});

View file

@ -0,0 +1,746 @@
'use strict';
/* global ActiveTargetsManager, AlternativesCharMenuManager, UserPressManager,
KeyboardConsole */
require('/js/keyboard/active_targets_manager.js');
require('/js/keyboard/alternatives_char_menu_manager.js');
require('/js/keyboard/handwriting_pads_manager.js');
require('/js/keyboard/user_press_manager.js');
require('/js/keyboard/console.js');
suite('ActiveTargetsManager', function() {
var app;
var manager;
var userPressManagerStub;
var alternativesCharMenuManagerStub;
setup(function() {
// Stub the depending constructors and have them returns a stub instance.
alternativesCharMenuManagerStub =
this.sinon.stub(AlternativesCharMenuManager.prototype);
this.sinon.stub(window, 'AlternativesCharMenuManager')
.returns(alternativesCharMenuManagerStub);
userPressManagerStub = this.sinon.stub(UserPressManager.prototype);
this.sinon.stub(window, 'UserPressManager').returns(userPressManagerStub);
this.sinon.stub(window, 'setTimeout', function () {
return 1;
});
this.sinon.stub(window, 'clearTimeout');
app = {
console: this.sinon.stub(KeyboardConsole.prototype)
};
manager = new ActiveTargetsManager(app);
manager.ontargetactivated = this.sinon.stub();
manager.ontargetlongpressed = this.sinon.stub();
manager.ontargetmoved = this.sinon.stub();
manager.ontargetmovedout = this.sinon.stub();
manager.ontargetmovedin = this.sinon.stub();
manager.ontargetcommitted = this.sinon.stub();
manager.ontargetcancelled = this.sinon.stub();
manager.ontargetdoubletapped = this.sinon.stub();
manager.onnewtargetwillactivate = this.sinon.stub();
manager.start();
assert.isTrue(window.UserPressManager.calledWithNew());
assert.isTrue(window.UserPressManager.calledWith(app));
assert.isTrue(userPressManagerStub.start.calledOnce);
assert.isTrue(window.AlternativesCharMenuManager.calledWithNew());
assert.isTrue(window.AlternativesCharMenuManager.calledWith(app));
assert.isTrue(alternativesCharMenuManagerStub.start.calledOnce);
});
test('stop', function() {
manager.stop();
assert.isTrue(userPressManagerStub.stop.calledOnce);
assert.isTrue(alternativesCharMenuManagerStub.stop.calledOnce);
assert.isTrue(window.clearTimeout.calledOnce);
});
suite('start first press', function() {
var id0 = 0;
var press0 = {
target: {
text: '1',
}
};
setup(function() {
alternativesCharMenuManagerStub.isShown = false;
userPressManagerStub.onpressstart(press0, id0);
assert.isTrue(
manager.ontargetactivated.calledWith(press0.target, press0));
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(
window.setTimeout.getCall(0).args[1], manager.LONG_PRESS_TIMEOUT);
});
test('press move (ignore same target), press end', function() {
userPressManagerStub.onpressmove(press0, id0);
assert.isFalse(window.clearTimeout.calledTwice);
assert.isFalse(window.setTimeout.calledTwice);
assert.isFalse(manager.ontargetmovedout.called);
assert.isFalse(manager.ontargetmovedin.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.calledTwice);
assert.isTrue(
manager.ontargetmoved.calledWith(press0.target, press0));
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledThrice);
});
test('press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledThrice);
});
test('press end (moved press)', function() {
var pressEnd = {
target: {
text: 'e'
},
moved: true
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledThrice);
});
test('press move to handwriting pad', function() {
var pressMove = {
target: {
isHandwritingPad: true
},
moved: true
};
userPressManagerStub.onpressmove(pressMove, id0);
assert.isTrue(window.clearTimeout.calledTwice);
assert.isTrue(window.setTimeout.calledTwice);
assert.isTrue(manager.ontargetmovedout.called);
assert.isTrue(manager.ontargetmovedin.called);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
});
test('double tap (within DOUBLE_TAP_TIMEOUT)', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isFalse(manager.ontargetdoubletapped.calledOnce,
'ontargetdoubletapped should not be called yet.');
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
userPressManagerStub.onpressstart(press0, id0);
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isTrue(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetdoubletapped.calledWith(press0.target));
});
test('double tap (after DOUBLE_TAP_TIMEOUT)', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isFalse(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.equal(window.setTimeout.getCall(1).args[1],
manager.DOUBLE_TAP_TIMEOUT);
window.setTimeout.getCall(1).args[0].call(window);
userPressManagerStub.onpressstart(press0, id0);
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledTwice);
assert.isFalse(manager.ontargetdoubletapped.calledOnce,
'ontargetdoubletapped should not be called.');
assert.isTrue(
manager.ontargetcommitted.getCall(1).calledWith(press0.target));
});
test('double tap (with an intermediate different target)', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isFalse(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.equal(window.setTimeout.getCall(1).args[1],
manager.DOUBLE_TAP_TIMEOUT);
// Touch another target in the middle
var press1 = {
target: {
text: '2'
}
};
userPressManagerStub.onpressstart(press1, id0);
userPressManagerStub.onpressend(press1, id0);
assert.isTrue(manager.ontargetcommitted.calledTwice);
assert.isTrue(
manager.ontargetcommitted.getCall(1).calledWith(press1.target));
userPressManagerStub.onpressstart(press0, id0);
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledThrice,
'ontargetcommitted should be called for the 3rd time.');
assert.isFalse(manager.ontargetdoubletapped.calledOnce,
'ontargetdoubletapped should not be called.');
assert.isTrue(
manager.ontargetcommitted.getCall(2).calledWith(press0.target));
});
test('tapping on a different target will clear ' +
'the double tap timer', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isFalse(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.equal(window.setTimeout.getCall(1).args[1],
manager.DOUBLE_TAP_TIMEOUT);
// Touch another target
var press1 = {
target: {
text: '2'
}
};
userPressManagerStub.onpressstart(press1, id0);
userPressManagerStub.onpressend(press1, id0);
assert.equal(window.clearTimeout.callCount, 6);
assert.isTrue(manager.ontargetcommitted.calledTwice);
assert.isTrue(
manager.ontargetcommitted.getCall(1).calledWith(press1.target));
});
test('triple tap (within DOUBLE_TAP_TIMEOUT)', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isFalse(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
userPressManagerStub.onpressstart(press0, id0);
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledOnce);
assert.isTrue(manager.ontargetdoubletapped.calledOnce);
assert.isTrue(
manager.ontargetdoubletapped.calledWith(press0.target));
userPressManagerStub.onpressstart(press0, id0);
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(manager.ontargetcommitted.calledTwice);
assert.isTrue(manager.ontargetdoubletapped.calledOnce,
'ontargetdoubletapped should not be called again.');
assert.isTrue(
manager.ontargetcommitted.getCall(1).calledWith(press0.target));
});
suite('clearAllTargets', function() {
setup(function() {
manager.clearAllTargets();
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcancelled.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledTwice);
});
test('ignore press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isFalse(manager.ontargetcommitted.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.calledTwice);
});
suite('ignore press moved', function() {
setup(function() {
var pressMove = {
target: {
text: 'm'
}
};
alternativesCharMenuManagerStub.isMenuTarget.returns(false);
alternativesCharMenuManagerStub.isInMenuArea.returns(false);
userPressManagerStub.onpressmove(pressMove, id0);
assert.isFalse(manager.ontargetmovedout.called);
assert.isFalse(manager.ontargetmovedin.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.calledTwice);
});
test('ignore press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isFalse(manager.ontargetcommitted.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.calledTwice);
});
});
});
suite('trigger long press, without alt char menu', function() {
setup(function() {
window.setTimeout.getCall(0).args[0].call(window);
assert.isTrue(
manager.ontargetlongpressed.calledWith(press0.target));
assert.isTrue(
alternativesCharMenuManagerStub.show.calledWith(press0.target));
});
test('press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledThrice);
});
});
suite('trigger long press, with alt char menu', function() {
var altTarget;
setup(function() {
altTarget = {
text: 'a'
};
alternativesCharMenuManagerStub.isShown = true;
alternativesCharMenuManagerStub
.isInMenuArea.withArgs(press0).returns(true);
alternativesCharMenuManagerStub.getMenuTarget.returns(altTarget);
alternativesCharMenuManagerStub
.isMenuTarget.withArgs(altTarget).returns(true);
window.setTimeout.getCall(0).args[0].call(window);
assert.isTrue(
manager.ontargetlongpressed.calledWith(press0.target));
assert.isTrue(
manager.ontargetmovedout.calledWith(press0.target));
assert.isTrue(
manager.ontargetmovedin.calledWith(altTarget));
});
test('press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(altTarget));
assert.equal(window.clearTimeout.callCount, 4);
});
suite('ignore second press', function() {
var id1 = 1;
var press1 = {
target: {
text: '2'
}
};
setup(function() {
userPressManagerStub.onpressstart(press1, id1);
assert.isFalse(manager.ontargetactivated.calledTwice);
assert.isFalse(window.clearTimeout.calledThrice);
assert.isFalse(window.setTimeout.calledThrice);
});
test('ignore second press, press end first press', function() {
var pressMove = {
target: {
text: 'm'
}
};
userPressManagerStub.onpressmove(pressMove, id1);
assert.isFalse(manager.ontargetmovedout.calledTwice);
assert.isFalse(manager.ontargetmovedin.calledTwice);
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id1);
assert.isFalse(manager.ontargetcommitted.called);
var pressEnd2 = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd2, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(altTarget));
assert.equal(window.clearTimeout.callCount, 4);
});
});
});
suite('press moved', function() {
var movedTarget;
var pressMove;
setup(function() {
alternativesCharMenuManagerStub.isMenuTarget.returns(false);
alternativesCharMenuManagerStub.isInMenuArea.returns(false);
var oldTarget = press0.target;
movedTarget = {
text: 'm'
};
pressMove = {
target: movedTarget
};
userPressManagerStub.onpressmove(pressMove, id0);
assert.isTrue(
manager.ontargetmovedout.calledWith(oldTarget));
assert.isTrue(
manager.ontargetmovedin.calledWith(movedTarget));
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(window.clearTimeout.calledTwice);
assert.isTrue(window.setTimeout.calledTwice);
});
test('press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledTwice);
assert.isTrue(
manager.ontargetcommitted.calledWith(movedTarget));
assert.equal(window.clearTimeout.callCount, 4);
});
suite('trigger long press, without alt char menu', function() {
setup(function() {
window.setTimeout.getCall(1).args[0].call(window);
assert.isTrue(
manager.ontargetlongpressed.calledWith(movedTarget));
assert.isTrue(
alternativesCharMenuManagerStub.show.calledWith(movedTarget));
});
test('press end', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledTwice);
assert.isTrue(
manager.ontargetcommitted.calledWith(movedTarget));
assert.equal(window.clearTimeout.callCount, 4);
});
});
});
suite('start second press', function() {
var id1 = 1;
var press1 = {
target: {
text: '2'
}
};
setup(function() {
userPressManagerStub.onpressstart(press1, id1);
assert.isTrue(manager.onnewtargetwillactivate.calledWith(press0.target),
'onnewtargetwillactivate called.');
assert.isTrue(manager.ontargetactivated.calledWith(press1.target));
assert.equal(window.clearTimeout.callCount, 2);
assert.equal(window.setTimeout.callCount, 1);
});
test('press end second press, and first press', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id1);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press1.target));
assert.equal(window.clearTimeout.callCount, 4);
var pressEnd2 = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd2, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledTwice,
'No additional calls to hide()');
assert.isTrue(manager.ontargetcommitted.calledTwice,
'No additional calls to ontargetcommitted');
});
test('press end first press, and second press', function() {
var pressEnd = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.equal(window.clearTimeout.callCount, 4);
var pressEnd2 = {
target: {
text: 'e'
}
};
userPressManagerStub.onpressend(pressEnd2, id1);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledTwice,
'No additional calls to hide()');
assert.isTrue(manager.ontargetcommitted.calledTwice,
'No additional calls to ontargetcommitted');
});
});
});
suite('start first press (on a selection)', function() {
var id0 = 0;
var press0 = {
target: {
text: '1',
selection: 'true'
}
};
setup(function() {
alternativesCharMenuManagerStub.isShown = false;
userPressManagerStub.onpressstart(press0, id0);
assert.isTrue(
manager.ontargetactivated.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(
window.setTimeout.getCall(0).args[1], manager.LONG_PRESS_TIMEOUT);
});
test('press end', function() {
var pressEnd = {
target: press0.target
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcommitted.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledThrice);
});
test('press move and press end (moved press)', function() {
var pressMove = {
target: press0.target,
moved: true
};
userPressManagerStub.onpressmove(pressMove, id0);
var pressEnd = {
target: press0.target,
moved: true
};
userPressManagerStub.onpressend(pressEnd, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(
manager.ontargetcancelled.calledWith(press0.target),
'target should be cancelled.');
assert.isTrue(window.clearTimeout.calledTwice);
});
});
suite('start first press on handwriting pad', function() {
var id0 = 0;
var press0 = {
target: {
isHandwritingPad: true
}
};
setup(function() {
alternativesCharMenuManagerStub.isShown = false;
userPressManagerStub.onpressstart(press0, id0);
assert.isTrue(
manager.ontargetactivated.calledWith(press0.target));
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(
window.setTimeout.getCall(0).args[1], manager.LONG_PRESS_TIMEOUT);
manager.blockNewUserPress = true;
manager.blockTargetMovedOut = true;
});
test('ignore new press after start handwriting', function() {
var newPress = {
target: {
text: 'n'
}
};
var id1 = 1;
userPressManagerStub.onpressstart(newPress, id1);
assert.isFalse(
manager.ontargetactivated.calledWith(newPress.target));
assert.isFalse(window.clearTimeout.calledTwice);
assert.isFalse(window.setTimeout.calledTwice);
});
test('press move on handwriting pad, press end', function() {
userPressManagerStub.onpressmove(press0, id0);
assert.isFalse(window.setTimeout.calledTwice);
assert.isFalse(manager.ontargetmovedout.called);
assert.isTrue(manager.ontargetmoved.called);
assert.isFalse(manager.ontargetmovedin.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.called);
// Press end
userPressManagerStub.onpressend(press0, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(window.clearTimeout.calledThrice);
assert.isTrue(window.setTimeout.calledTwice);
assert.isTrue(manager.ontargetcommitted.called);
});
test('press move out handwriting pad, press end', function() {
var pressMove = {
target: {
text: 'm'
}
};
userPressManagerStub.onpressmove(pressMove, id0);
assert.isFalse(window.setTimeout.calledTwice);
assert.isFalse(manager.ontargetmovedout.called);
assert.isFalse(manager.ontargetmovedin.called);
assert.isFalse(alternativesCharMenuManagerStub.hide.called);
// Press end
userPressManagerStub.onpressend(press0, id0);
assert.isTrue(alternativesCharMenuManagerStub.hide.calledOnce);
assert.isTrue(window.clearTimeout.calledThrice);
assert.isTrue(window.setTimeout.calledTwice);
assert.isTrue(manager.ontargetcommitted.called);
});
});
});

View file

@ -0,0 +1,355 @@
'use strict';
/* global AlternativesCharMenuManager */
require('/js/keyboard/alternatives_char_menu_manager.js');
suite('AlternativesCharMenuManager', function() {
var app;
var container;
var manager;
var target;
var targetElem;
var getFakeElementWithGetBoundingClientRect;
var dummyObjForChild1;
var fakeMenuView;
var viewManager;
setup(function() {
getFakeElementWithGetBoundingClientRect = function() {
return {
getBoundingClientRect: this.sinon.stub(),
dataset: {
lowercaseValue: 'x',
uppercaseValue: 'X'
}
};
}.bind(this);
// Create fake menu container element
container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '5px';
container.style.top = '40px';
container.style.width = '60px';
container.style.height = '10px';
container.id = 'test-menu';
var child1 = document.createElement('div');
container.appendChild(child1);
var child2 = document.createElement('div');
container.appendChild(child2);
dummyObjForChild1 = {
dummy: 'dummy'
};
document.body.appendChild(container);
var viewMap = new WeakMap();
fakeMenuView = {
getMenuContainer: function() {
return container;
},
getBoundingClientRect: function() {
return container.getBoundingClientRect();
},
getMenuTarget: this.sinon.stub(),
isMenuTarget: this.sinon.stub()
};
// Create fake viewManager
viewManager = {
showAlternativesCharMenu: function(target, alternatives) {
return fakeMenuView;
},
hideAlternativesCharMenu: this.sinon.stub(),
registerView: function(target, view) {
viewMap.set(target, view);
},
getView(target) {
return viewMap.get(target);
}
};
this.sinon.spy(viewManager, 'showAlternativesCharMenu');
// Create fake app
app = {
upperCaseStateManager: {
isUpperCaseLocked: undefined,
isUpperCase: undefined
},
layoutManager: {
currentPage: {
alt: {
}
}
},
layoutRenderingManager: {
getTargetObject: this.sinon.stub()
},
viewManager: viewManager
};
// Create fake target object and DOM element
targetElem = getFakeElementWithGetBoundingClientRect();
targetElem.getBoundingClientRect.returns({
top: 50,
bottom: 60,
left: 10,
right: 40,
width: 30
});
target = {
keyCode: 'x'.charCodeAt(0),
keyCodeUpper: 'X'.charCodeAt(0),
lowercaseValue: 'x',
uppercaseValue: 'X'
};
viewManager.registerView(target, {element: targetElem});
// Show an alternatives chars menu
manager = new AlternativesCharMenuManager(app);
manager.start();
assert.equal(manager.isShown, false);
});
teardown(function() {
app = null;
container = null;
});
test('show (lower case)', function() {
app.upperCaseStateManager.isUpperCase = false;
app.upperCaseStateManager.isUpperCaseLocked = false;
app.layoutManager.currentPage.alt.x =
['a', 'b', 'c', 'd'];
manager.show(target);
assert.isTrue(viewManager.
showAlternativesCharMenu.calledWith(target, ['x', 'a', 'b', 'c', 'd']));
assert.isTrue(app.layoutManager.currentPage.alt.x !==
viewManager.showAlternativesCharMenu.getCall(0).args[1],
'A copy of the array should be sent instead of the original one.');
assert.isTrue(manager.isShown);
});
test('show (upper case)', function() {
app.upperCaseStateManager.isUpperCase = true;
app.upperCaseStateManager.isUpperCaseLocked = false;
app.layoutManager.currentPage.alt.X =
['A', 'B', 'C', 'D'];
app.layoutManager.currentPage.alt.X.upperCaseLocked =
['E', 'F', 'G', 'H'];
manager.show(target);
assert.isTrue(viewManager.
showAlternativesCharMenu.calledWith(target, ['X', 'A', 'B', 'C', 'D']));
assert.isTrue(app.layoutManager.currentPage.alt.x !==
viewManager.showAlternativesCharMenu.getCall(0).args[1],
'A copy of the array should be sent instead of the original one.');
assert.isTrue(manager.isShown);
});
test('show (upper case locked)', function() {
app.upperCaseStateManager.isUpperCase = true;
app.upperCaseStateManager.isUpperCaseLocked = true;
app.layoutManager.currentPage.alt.X =
['A', 'B', 'C', 'D'];
app.layoutManager.currentPage.alt.X.upperCaseLocked =
['E', 'F', 'G', 'H'];
manager.show(target);
assert.isTrue(viewManager.
showAlternativesCharMenu.calledWith(target, ['X', 'E', 'F', 'G', 'H']));
assert.isTrue(app.layoutManager.currentPage.alt.x !==
viewManager.showAlternativesCharMenu.getCall(0).args[1],
'A copy of the array should be sent instead of the original one.');
assert.isTrue(manager.isShown);
});
test('show (ignore key w/o alternatives)', function() {
app.upperCaseStateManager.isUpperCase = false;
app.upperCaseStateManager.isUpperCaseLocked = false;
manager.show(target);
assert.isFalse(viewManager.showAlternativesCharMenu.called);
assert.isFalse(manager.isShown);
});
suite('after shown', function() {
setup(function() {
app.upperCaseStateManager.isUpperCase = false;
app.upperCaseStateManager.isUpperCaseLocked = false;
app.layoutManager.currentPage.alt.x =
['a', 'b', 'c', 'd'];
manager.show(target);
});
test('hide', function() {
manager.hide();
assert.equal(manager.isShown, false);
assert.isTrue(viewManager.hideAlternativesCharMenu.calledOnce);
});
suite('isMenuTarget', function() {
var dummyTarget = {
dummy: null
};
setup(function() {
manager.isMenuTarget(dummyTarget);
});
test('will invoke MenuView\'s isMenuTarget', function() {
assert.isTrue(fakeMenuView.isMenuTarget.calledOnce);
assert.isTrue(fakeMenuView.isMenuTarget.calledWith(dummyTarget));
});
});
suite('isInMenuArea', function() {
test('above menu', function() {
var press = {
clientX: 45,
clientY: 35
};
assert.equal(manager.isInMenuArea(press), false);
});
test('below key', function() {
var press = {
clientX: 45,
clientY: 70
};
assert.equal(manager.isInMenuArea(press), false);
});
test('left of menu', function() {
var press = {
clientX: 2,
clientY: 55
};
assert.equal(manager.isInMenuArea(press), true);
});
test('right of menu', function() {
var press = {
clientX: 80,
clientY: 55
};
assert.equal(manager.isInMenuArea(press), true);
});
test('right of menu, exceeds the key width', function() {
var press = {
clientX: 150,
clientY: 55
};
assert.equal(manager.isInMenuArea(press), false);
});
test('on top of the menu', function() {
var press = {
clientX: 45,
clientY: 40
};
// We've extend the locked area to include the menu itself.
assert.equal(manager.isInMenuArea(press), true);
});
test('on top of the key', function() {
var press = {
clientX: 15,
clientY: 55
};
assert.equal(manager.isInMenuArea(press), true);
});
test('below menu and beside key', function() {
var press = {
clientX: 65,
clientY: 55
};
assert.equal(manager.isInMenuArea(press), true);
});
test('below menu and above key', function() {
var press = {
clientX: 65,
clientY: 47
};
assert.equal(manager.isInMenuArea(press), true);
});
});
suite('getMenuTarget', function() {
test('will invoke menuView.getMenuTarget', function() {
var press = {
target: {},
clientX: 35,
clientY: 55
};
manager.getMenuTarget(press);
assert.isTrue(fakeMenuView.getMenuTarget.calledOnce);
assert.isTrue(fakeMenuView.getMenuTarget.calledWith(press.clientX,
press.clientY));
});
});
});
suite('after hiding the menu', function() {
setup(function() {
app.upperCaseStateManager.isUpperCase = false;
app.upperCaseStateManager.isUpperCaseLocked = false;
app.layoutManager.currentPage.alt.x =
['a', 'b', 'c', 'd'];
manager.show(target);
manager.hide();
});
test('isMenuTarget should be false', function() {
assert.isFalse(manager.isMenuTarget(target));
});
test('isInMenuArea() should return false', function() {
var press = {
clientX: 65,
clientY: 47
};
assert.isFalse(manager.isInMenuArea(press));
});
});
});

View file

@ -0,0 +1,256 @@
'use strict';
/* global CandidatePanelManager, MockEventTarget */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/js/keyboard/candidate_panel_manager.js');
suite('CandidatePanelManager', function() {
var app;
var manager;
var suggestionsContainer;
var viewManager;
setup(function() {
suggestionsContainer = new MockEventTarget();
this.sinon.spy(suggestionsContainer, 'addEventListener');
this.sinon.spy(suggestionsContainer, 'removeEventListener');
viewManager = {
showMoreCandidates: this.sinon.stub(),
candidatePanel: {},
getNumberOfCandidatesPerRow: this.sinon.stub(),
toggleCandidatePanel: this.sinon.stub()
};
viewManager.getNumberOfCandidatesPerRow.returns(8);
viewManager.candidatePanel.dataset = {};
viewManager.candidatePanel.scrollHeight = 500;
viewManager.candidatePanel.clientHeight = 100;
viewManager.candidatePanel.scrollTop = 398;
viewManager.candidatePanel.querySelector = this.sinon.stub();
viewManager.candidatePanel.querySelector.returns(
suggestionsContainer);
this.sinon.stub(window, 'setTimeout');
this.sinon.stub(window, 'clearTimeout');
app = {
inputMethodManager: {
currentIMEngine: {}
},
viewManager: viewManager
};
manager = new CandidatePanelManager(app);
manager.oncandidateschange = this.sinon.stub();
manager.start();
});
teardown(function() {
app = null;
});
test('stop', function() {
manager.stop();
assert.isTrue(window.clearTimeout.calledOnce);
});
suite('updateCandidates', function() {
var candidates;
setup(function() {
candidates = (function() {
var arr = [];
var i = 500;
while (i--) {
arr.push(['candidate ' + i, i]);
}
return arr;
}());
manager.updateCandidates(candidates);
assert.equal(manager.currentCandidates, candidates);
assert.isTrue(manager.oncandidateschange.calledOnce);
viewManager.candidatePanel.dataset.truncated = 'true';
viewManager.candidatePanel.dataset.rowCount = '1';
});
test('reset', function() {
this.sinon.stub(manager, 'hideFullPanel');
manager.reset();
assert.isTrue(manager.hideFullPanel.calledOnce);
assert.deepEqual(manager.currentCandidates, []);
});
test('toggleFullPanel', function() {
this.sinon.stub(manager, 'showFullPanel');
this.sinon.stub(manager, 'hideFullPanel');
manager.toggleFullPanel();
assert.isTrue(manager.showFullPanel.calledOnce);
manager.isFullPanelShown = true;
manager.toggleFullPanel();
assert.isTrue(manager.hideFullPanel.calledOnce);
});
suite('With engine.getMoreCandidates',function() {
var getMoreCandidatesStub;
setup(function() {
getMoreCandidatesStub =
app.inputMethodManager.currentIMEngine.getMoreCandidates =
this.sinon.stub();
// Assume the candidates are truncated at 10th candidate.
viewManager.candidatePanel.dataset.candidateIndicator = '10';
});
suite('showFullPanel', function() {
setup(function() {
manager.showFullPanel();
assert.isTrue(manager.isFullPanelShown);
assert.isTrue(getMoreCandidatesStub.calledWith(
10, manager.FIRST_PAGE_ROWS * 8 + 1));
var firstPageList =
candidates.slice(10, 10 + manager.FIRST_PAGE_ROWS * 8 + 1);
getMoreCandidatesStub.getCall(0).args[2].call(window, firstPageList);
assert.isTrue(viewManager.showMoreCandidates
.calledWith(manager.FIRST_PAGE_ROWS, firstPageList));
assert.isTrue(viewManager.toggleCandidatePanel.calledWith(true));
assert.isTrue(suggestionsContainer.addEventListener.calledOnce);
});
suite('showNextCandidatePage', function() {
setup(function() {
viewManager.candidatePanel.dataset.rowCount = '2';
viewManager.candidatePanel.dataset.candidateIndicator = '90';
var scrollEvent = {
type: 'scroll'
};
suggestionsContainer.dispatchEvent(scrollEvent);
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(window.setTimeout.getCall(0).args[1], 200);
window.setTimeout.getCall(0).args[0].call(window);
assert.isTrue(getMoreCandidatesStub.getCall(1).calledWith(
90, manager.PAGE_ROWS * 8 + 1));
var nextPageList =
candidates.slice(90, 90 + manager.PAGE_ROWS * 8 + 1);
getMoreCandidatesStub.getCall(1).args[2].call(window, nextPageList);
assert.isTrue(viewManager.showMoreCandidates
.calledWith(manager.PAGE_ROWS, nextPageList));
});
test('hideFullPanel & show panel', function () {
viewManager.candidatePanel.dataset.rowCount = '3';
viewManager.candidatePanel.dataset.candidateIndicator = '187';
manager.hideFullPanel();
assert.isFalse(manager.isFullPanelShown);
assert.isTrue(window.clearTimeout.calledTwice);
assert.isTrue(suggestionsContainer.removeEventListener.calledOnce);
manager.showFullPanel();
assert.isTrue(viewManager.showMoreCandidates.calledTwice,
'No showMoreCandidates call here.');
assert.isTrue(suggestionsContainer.addEventListener.calledTwice);
});
});
test('hideFullPanel', function () {
manager.hideFullPanel();
assert.isFalse(manager.isFullPanelShown);
assert.isTrue(suggestionsContainer.removeEventListener.calledOnce);
});
});
});
suite('No engine.getMoreCandidates',function() {
setup(function() {
// Assume the candidates are truncated at 10th candidate.
viewManager.candidatePanel.dataset.candidateIndicator = '10';
});
suite('showFullPanel', function() {
setup(function() {
manager.showFullPanel();
var firstPageList =
candidates.slice(10, 10 + manager.FIRST_PAGE_ROWS * 8 + 1);
assert.isTrue(manager.isFullPanelShown);
assert.isTrue(viewManager.showMoreCandidates
.calledWith(manager.FIRST_PAGE_ROWS, firstPageList));
assert.isTrue(
viewManager.toggleCandidatePanel.calledWith(true));
assert.isTrue(suggestionsContainer.addEventListener.calledOnce);
});
suite('showNextCandidatePage', function() {
setup(function() {
viewManager.candidatePanel.dataset.rowCount = '2';
viewManager.candidatePanel.dataset.candidateIndicator = '90';
var scrollEvent = {
type: 'scroll'
};
suggestionsContainer.dispatchEvent(scrollEvent);
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(window.setTimeout.calledOnce);
window.setTimeout.getCall(0).args[0].call(window);
var nextPageList =
candidates.slice(90, 90 + manager.PAGE_ROWS * 8 + 1);
assert.isTrue(viewManager.showMoreCandidates
.calledWith(manager.PAGE_ROWS, nextPageList));
});
test('hideFullPanel & show panel', function () {
viewManager.candidatePanel.dataset.rowCount = '3';
viewManager.candidatePanel.dataset.candidateIndicator = '187';
manager.hideFullPanel();
assert.isFalse(manager.isFullPanelShown);
assert.isTrue(window.clearTimeout.calledTwice);
assert.isTrue(suggestionsContainer.removeEventListener.calledOnce);
manager.showFullPanel();
assert.isTrue(viewManager.showMoreCandidates.calledTwice,
'No showMoreCandidates call here.');
assert.isTrue(suggestionsContainer.addEventListener.calledTwice);
});
});
test('hideFullPanel', function () {
manager.hideFullPanel();
assert.isFalse(manager.isFullPanelShown);
assert.isTrue(window.clearTimeout.calledOnce);
assert.isTrue(suggestionsContainer.removeEventListener.calledOnce);
});
});
});
});
});

View file

@ -0,0 +1,315 @@
'use strict';
/* global VibrationFeedback, SoundFeedback, FeedbackManager,
MockNavigatorMozSettings, MockNavigatorMozSettingsLock,
SettingsPromiseManager, SoundFeedbackPlayer */
require('/js/keyboard/feedback_manager.js');
require('/js/keyboard/settings.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_dom_request.js');
require('/shared/js/input_mgmt/mock_navigator_mozsettings.js');
require('/js/keyboard/sound_feedback_player.js');
suite('VibrationFeedback', function() {
var realMozSettings;
var feedback;
var mozSettings;
var lock;
suiteSetup(function() {
realMozSettings = navigator.mozSettings;
});
suiteTeardown(function() {
navigator.mozSettings = realMozSettings;
});
setup(function() {
mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
lock = new MockNavigatorMozSettingsLock();
this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
feedback = new VibrationFeedback({
settingsPromiseManager: promiseManager
});
feedback.start();
});
suite('init with vibrate=true', function() {
setup(function(done) {
var req = lock.get.getCall(0).returnValue;
req.fireSuccess({ 'keyboard.vibration': true });
feedback.settings.initSettings().then(function() {
}).then(done, done);
});
test('vibrate', function() {
this.sinon.stub(navigator, 'vibrate');
feedback.triggerFeedback();
assert.isTrue(navigator.vibrate.calledOnce);
});
test('change to vibrate=false', function() {
mozSettings.dispatchSettingChange('keyboard.vibration', false);
this.sinon.stub(navigator, 'vibrate');
feedback.triggerFeedback();
assert.equal(navigator.vibrate.calledOnce, false);
});
});
suite('init with vibrate=false', function() {
setup(function(done) {
var req = lock.get.getCall(0).returnValue;
req.fireSuccess({ 'keyboard.vibration': false });
feedback.settings.initSettings().then(function() {
}).then(done, done);
});
test('vibrate', function() {
this.sinon.stub(navigator, 'vibrate');
feedback.triggerFeedback();
assert.equal(navigator.vibrate.calledOnce, false);
});
test('change to vibrate=true', function() {
mozSettings.dispatchSettingChange('keyboard.vibration', true);
this.sinon.stub(navigator, 'vibrate');
feedback.triggerFeedback();
assert.isTrue(navigator.vibrate.calledOnce);
});
});
test('not ready -- don\'t vibrate', function() {
this.sinon.stub(navigator, 'vibrate');
feedback.triggerFeedback();
assert.equal(navigator.vibrate.calledOnce, false);
});
});
suite('SoundFeedback', function() {
var realMozSettings;
var feedback;
var mozSettings;
var lock;
var normalTarget;
var specialTarget;
var stubSoundFeedbackPlayer;
suiteSetup(function() {
realMozSettings = navigator.mozSettings;
});
suiteTeardown(function() {
navigator.mozSettings = realMozSettings;
});
setup(function() {
mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
lock = new MockNavigatorMozSettingsLock();
this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
stubSoundFeedbackPlayer =
this.sinon.stub(Object.create(SoundFeedbackPlayer.prototype));
this.sinon.stub(window, 'SoundFeedbackPlayer')
.returns(stubSoundFeedbackPlayer);
normalTarget = {
keyCode: 60,
isSpecialKey: false
};
specialTarget = {
keyCode: 60,
isSpecialKey: true
};
feedback = new SoundFeedback({
settingsPromiseManager: promiseManager
});
feedback.start();
});
suite('init with sound=true', function() {
setup(function(done) {
var req = lock.get.getCall(0).returnValue;
req.fireSuccess({ 'keyboard.clicksound': true });
var req1 = lock.get.getCall(1).returnValue;
req1.fireSuccess({ 'audio.volume.notification': 10 });
feedback.settings.initSettings().then(function() {
}).then(function() {
assert.isTrue(stubSoundFeedbackPlayer.prepare.calledOnce);
}).then(done, done);
});
test('sound (normal key)', function() {
feedback.triggerFeedback(normalTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(false));
});
test('sound (special key)', function() {
feedback.triggerFeedback(specialTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(true));
});
test('change to sound=false', function() {
mozSettings.dispatchSettingChange('keyboard.clicksound', false);
assert.equal(feedback.player, null, 'player should be dropped');
feedback.triggerFeedback(normalTarget);
feedback.triggerFeedback(specialTarget);
assert.isFalse(stubSoundFeedbackPlayer.play.called);
});
suite('activate', function() {
setup(function() {
feedback.activate();
assert.isTrue(stubSoundFeedbackPlayer.activate.called);
});
test('deactivate', function() {
feedback.deactivate();
assert.isTrue(stubSoundFeedbackPlayer.deactivate.called);
});
});
});
suite('init with sound=false', function() {
setup(function(done) {
var req = lock.get.getCall(0).returnValue;
req.fireSuccess({ 'keyboard.clicksound': false });
var req1 = lock.get.getCall(1).returnValue;
req1.fireSuccess({ 'audio.volume.notification': 10 });
feedback.settings.initSettings().then(function() {
}).then(function() {
assert.isFalse(stubSoundFeedbackPlayer.prepare.calledOnce);
}).then(done, done);
});
test('sound (normal key)', function() {
assert.equal(feedback.player, null, 'player should be dropped');
feedback.triggerFeedback(normalTarget);
assert.isFalse(stubSoundFeedbackPlayer.play.called);
});
test('sound (special key)', function() {
assert.equal(feedback.player, null, 'player should be dropped');
feedback.triggerFeedback(specialTarget);
assert.isFalse(stubSoundFeedbackPlayer.play.called);
});
test('change to sound=true and sound (normal key)', function() {
mozSettings.dispatchSettingChange('keyboard.clicksound', true);
assert.isTrue(stubSoundFeedbackPlayer.prepare.calledOnce);
feedback.triggerFeedback(normalTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(false));
});
test('change to sound=true and sound (special key)', function() {
mozSettings.dispatchSettingChange('keyboard.clicksound', true);
assert.isTrue(stubSoundFeedbackPlayer.prepare.calledOnce);
feedback.triggerFeedback(specialTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(true));
});
suite('activate', function() {
setup(function() {
feedback.activate();
});
test('deactivate', function() {
feedback.deactivate();
});
});
});
});
suite('FeedbackManager', function() {
test('start, feedback, stop', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var stubSoundFeedbackPlayer =
this.sinon.stub(Object.create(SoundFeedbackPlayer.prototype));
this.sinon.stub(window, 'SoundFeedbackPlayer')
.returns(stubSoundFeedbackPlayer);
this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var normalTarget = {
keyCode: 60,
isSpecialKey: false
};
var specialTarget = {
keyCode: 60,
isSpecialKey: true
};
var app = {
settingsPromiseManager: new SettingsPromiseManager()
};
var feedbackManager = new FeedbackManager(app);
feedbackManager.start();
feedbackManager.activate();
var req = lock.get.getCall(0).returnValue;
req.fireSuccess({ 'keyboard.vibration': true });
var req1 = lock.get.getCall(1).returnValue;
req1.fireSuccess({ 'keyboard.clicksound': true });
var req2 = lock.get.getCall(2).returnValue;
req2.fireSuccess({ 'audio.volume.notification': 10 });
feedbackManager.soundFeedback.settings.initSettings().then(function() {
assert.isTrue(stubSoundFeedbackPlayer.activate.called);
this.sinon.stub(navigator, 'vibrate');
feedbackManager.triggerFeedback(normalTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(false));
assert.isTrue(navigator.vibrate.calledOnce);
feedbackManager.triggerFeedback(specialTarget);
assert.isTrue(stubSoundFeedbackPlayer.play.calledWith(true));
assert.isTrue(navigator.vibrate.calledTwice);
feedbackManager.stop();
}.bind(this)).then(done, done);
});
});

View file

@ -0,0 +1,69 @@
'use strict';
/* global HandwritingPadsManager, SettingsPromiseManager */
require('/js/keyboard/handwriting_pads_manager.js');
require('/js/keyboard/settings.js');
suite('HandwritingPadsManager ', function() {
var app;
var manager;
var point;
setup(function() {
// Create fake app
app = {
settingsPromiseManager: new SettingsPromiseManager(),
layoutRenderingManager: {
drawHandwritingPad: function() {
return point;
}
}
};
this.sinon.stub(window, 'setTimeout');
this.sinon.stub(window, 'clearTimeout');
manager = new HandwritingPadsManager(app);
manager.start();
});
teardown(function() {
app = null;
manager = null;
point = null;
});
test('handle press events', function() {
point = {
posX: 20,
posY: 20
};
var press = {
clientX: 10,
clientY: 10
};
// Press start
manager.handlePressStart(press);
assert.isTrue(window.clearTimeout.calledOnce);
assert.equal(manager._strokePoints[0], 20);
assert.equal(manager._strokePoints[1], 20);
point = {
posX: 25,
posY: 25
};
// Press move
manager.handlePressMove(press);
assert.equal(manager._strokePoints[2], 25);
assert.equal(manager._strokePoints[3], 25);
// Press End
manager.handlePressEnd();
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(manager._strokePoints[4], -1);
assert.equal(manager._strokePoints[5], 0);
});
});

View file

@ -0,0 +1,380 @@
'use strict';
/* global InputMethodDatabaseLoader, InputMethodDatabaseConfig,
PromiseStorage */
require('/js/shared/promise_storage.js');
require('/js/keyboard/input_method_database_loader.js');
suite('InputMethodDatabaseConfig', function() {
var config;
suite('With config', function() {
var fakeXhr;
suite('Fully preloaded config', function() {
setup(function() {
var requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
config = new InputMethodDatabaseConfig();
config.start();
assert.equal(requests.length, 1);
var request = requests[0];
assert.equal(request.url, config.CONFIG_FILE_PATH);
assert.equal(request.responseType, 'json');
request.response = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451400,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'es',
'name': 'Español',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'es.dict',
'dictFilePath': 'dictionaries/es.dict',
'dictFileSize': 1564842,
'types': ['email', 'password', 'text', 'url']
}
];
request.respond(200, {}, '');
});
teardown(function() {
config.stop();
config = null;
fakeXhr.restore();
});
test('isAllDataPreloaded', function(done) {
config.isAllDataPreloaded().then(function(val) {
assert.ok(val, 'preload=true.');
}, function(e) {
throw e || 'Should not reject.';
}).then(done, done);
});
});
suite('Partly preloaded config', function() {
setup(function() {
var requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
config = new InputMethodDatabaseConfig();
config.start();
assert.equal(requests.length, 1);
var request = requests[0];
assert.equal(request.url, config.CONFIG_FILE_PATH);
assert.equal(request.responseType, 'json');
request.response = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451400,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'es',
'name': 'Español',
'imEngineId': 'latin',
'preloaded': false,
'dictFilename': 'es.dict',
'dictFilePath': 'dictionaries/es.dict',
'dictFileSize': 1564842,
'types': ['email', 'password', 'text', 'url']
}
];
request.respond(200, {}, '');
});
teardown(function() {
config.stop();
config = null;
fakeXhr.restore();
});
test('isDataPreloaded=true (unlisted)', function(done) {
config.isDataPreloaded('latin', 'dictionaries/foo.dict')
.then(function(val) {
assert.ok(val);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
test('isDataPreloaded=true', function(done) {
config.isDataPreloaded('latin', 'dictionaries/en_us.dict')
.then(function(val) {
assert.ok(val);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
test('isDataPreloaded=false', function(done) {
config.isDataPreloaded('latin', 'dictionaries/es.dict')
.then(function(val) {
assert.isFalse(val);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
test('isAllDataPreloaded', function(done) {
config.isAllDataPreloaded().then(function(val) {
assert.isFalse(val, 'preload=false.');
}, function(e) {
throw e || 'Should not reject.';
}).then(done, done);
});
});
});
suite('Without config', function() {
setup(function() {
config = new InputMethodDatabaseConfig();
config.CONFIG_FILE_PATH = '404.json';
config.start();
});
teardown(function() {
config.stop();
config = null;
});
test('isDataPreloaded', function(done) {
config.isDataPreloaded('foo', 'bar').then(function(val) {
assert.ok(val, 'preload=true without config file.');
}, function(e) {
throw e || 'Should not reject.';
}).then(done, done);
});
test('isAllDataPreloaded', function(done) {
config.isAllDataPreloaded().then(function(val) {
assert.ok(val, 'preload=true without config file.');
}, function(e) {
throw e || 'Should not reject.';
}).then(done, done);
});
});
});
suite('InputMethodDatabaseLoader', function() {
var promiseStorageStub;
var loader;
setup(function() {
promiseStorageStub =
this.sinon.stub(Object.create(PromiseStorage.prototype));
this.sinon.stub(window, 'PromiseStorage').returns(promiseStorageStub);
});
suite('Without config', function() {
setup(function() {
loader = new InputMethodDatabaseLoader();
// Use absolute path here.
loader.SOURCE_DIR = '/js/imes/';
loader.start();
});
teardown(function() {
loader.stop();
loader = null;
});
test('load data from package', function(done) {
loader.load('latin', 'dictionaries/en_us.dict').then(function(data) {
assert.equal(data && data.byteLength, 1451400, 'Got data');
}, function(e) {
if (e) { throw e; }
throw 'Should not reject';
}).then(done, done);
});
test('load data from package (non-exist path)', function(done) {
loader.load('latin', 'dictionaries/404.dict').then(function(data) {
assert.isTrue(false, 'Should not resolve');
}, function() {
assert.isTrue(true, 'Rejected');
}).then(done, done);
});
});
suite('With config', function() {
var fakeXhr;
suite('Fully preloaded config', function() {
setup(function() {
var requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
loader = new InputMethodDatabaseLoader();
// Use absolute path here.
loader.SOURCE_DIR = '/js/imes/';
loader.start();
assert.equal(requests.length, 1);
var request = requests[0];
request.response = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451400,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'es',
'name': 'Español',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'es.dict',
'dictFilePath': 'dictionaries/es.dict',
'dictFileSize': 1564842,
'types': ['email', 'password', 'text', 'url']
}
];
request.respond(200, {}, '');
// Restore here so that we can use the real XHR to get en_us.dict.
fakeXhr.restore();
});
teardown(function() {
assert.isFalse(window.PromiseStorage.called,
'PromiseStorage is not initialized.');
loader.stop();
loader = null;
});
test('load data from package', function(done) {
loader.load('latin', 'dictionaries/en_us.dict').then(function(data) {
assert.equal(data && data.byteLength, 1451400, 'Got data');
}, function(e) {
if (e) { throw e; }
throw 'Should not reject';
}).then(done, done);
});
});
suite('Partly preloaded config', function() {
setup(function() {
var requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
loader = new InputMethodDatabaseLoader();
// Use absolute path here.
loader.SOURCE_DIR = '/js/imes/';
loader.start();
assert.equal(requests.length, 1);
var request = requests[0];
request.response = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451400,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'es',
'name': 'Español',
'imEngineId': 'latin',
'preloaded': false,
'dictFilename': 'es.dict',
'dictFilePath': 'dictionaries/es.dict',
'dictFileSize': 1564842,
'types': ['email', 'password', 'text', 'url']
}
];
request.respond(200, {}, '');
// Restore here so that we can use the real XHR to get en_us.dict.
fakeXhr.restore();
});
teardown(function() {
assert.ok(window.PromiseStorage.calledWith(loader.DATABASE_NAME));
loader.stop();
loader = null;
});
test('load data from package', function(done) {
loader.load('latin', 'dictionaries/en_us.dict').then(function(data) {
assert.equal(data && data.byteLength, 1451400, 'Got data');
}, function(e) {
if (e) { throw e; }
throw 'Should not reject';
}).then(done, done);
});
test('load data from PromiseStorage', function(done) {
promiseStorageStub.getItem.returns(Promise.resolve({ stub: 'esDict '}));
loader.load('latin', 'dictionaries/es.dict').then(function(data) {
assert.deepEqual(data, { stub: 'esDict '}, 'Got data');
assert.ok(promiseStorageStub.getItem.calledWith(
'latin/dictionaries/es.dict'));
}, function(e) {
if (e) { throw e; }
throw 'Should not reject';
}).then(done, done);
});
});
});
});

View file

@ -0,0 +1,655 @@
'use strict';
/* global InputMethodGlue, InputMethodLoader, InputMethodManager,
KeyboardConsole, IMEngineSettings, KeyEvent, Promise */
require('/js/keyboard/settings.js');
require('/js/keyboard/input_method_manager.js');
require('/js/keyboard/console.js');
suite('InputMethodGlue', function() {
test('init', function() {
var glue = new InputMethodGlue();
var app = {};
glue.init(app, 'foo');
assert.equal(glue.app, app);
assert.equal(glue.path, './js/imes/foo');
});
test('sendCandidates', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
candidatePanelManager: {
updateCandidates: this.sinon.stub()
},
inputContext: {}
};
var data = [['foo', 1]];
glue.init(app, 'foo');
glue.sendCandidates(data);
assert.isTrue(app.candidatePanelManager.updateCandidates.calledWith(data));
});
test('setComposition', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
setComposition: this.sinon.stub().returns(Promise.resolve())
}
};
var symbols = 'bar';
var cursor = 1;
glue.init(app, 'foo');
glue.setComposition(symbols, cursor);
assert.isTrue(app.inputContext.setComposition.calledWith(symbols, cursor));
});
test('endComposition', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
endComposition: this.sinon.stub().returns(Promise.resolve())
}
};
var data = 'bar';
glue.init(app, 'foo');
glue.endComposition(data);
assert.isTrue(app.inputContext.endComposition.calledWith(data));
});
suite('sendKey', function() {
var glue;
var app;
var p;
setup(function() {
glue = new InputMethodGlue();
app = {
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
sendKey: this.sinon.stub()
}
};
p = Promise.resolve();
this.sinon.stub(p, 'catch').returns(p);
app.inputContext.sendKey.returns(p);
});
test('KeyEvent.DOM_VK_BACK_SPACE', function() {
var keyCode = KeyEvent.DOM_VK_BACK_SPACE;
var isRepeat = false;
glue.init(app, 'foo');
var returned = glue.sendKey(keyCode, isRepeat);
assert.isTrue(
app.inputContext.sendKey.calledWith(keyCode, 0, 0, isRepeat));
assert.equal(returned, p);
});
test('KeyEvent.DOM_VK_RETURN', function() {
var keyCode = KeyEvent.DOM_VK_RETURN;
var isRepeat = false;
glue.init(app, 'foo');
var returned = glue.sendKey(keyCode, isRepeat);
assert.isTrue(
app.inputContext.sendKey.calledWith(keyCode, 0, 0));
assert.equal(returned, p);
});
test('99', function() {
var keyCode = 99;
var isRepeat = false;
glue.init(app, 'foo');
var returned = glue.sendKey(keyCode, isRepeat);
assert.isTrue(
app.inputContext.sendKey.calledWith(0, 99, 0));
assert.equal(returned, p);
});
test('-99', function() {
var keyCode = -99;
var isRepeat = false;
glue.init(app, 'foo');
var returned = glue.sendKey(keyCode, isRepeat);
assert.isTrue(
app.inputContext.sendKey.calledWith(0, -99, 0));
assert.equal(returned, p);
});
test('(dict)', function() {
glue.init(app, 'foo');
var dict = {};
var returned = glue.sendKey(dict);
assert.isTrue(
app.inputContext.sendKey.firstCall.args[0] === dict);
assert.equal(returned, p);
});
});
test('sendString', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
sendKey: this.sinon.stub().returns(Promise.resolve())
}
};
var text = 'foobar';
glue.init(app, 'foo');
glue.sendString(text);
assert.isTrue(
app.inputContext.sendKey.getCall(0).calledWith(0, text.charCodeAt(0), 0));
assert.isTrue(
app.inputContext.sendKey.getCall(1).calledWith(0, text.charCodeAt(1), 0));
assert.isTrue(
app.inputContext.sendKey.getCall(2).calledWith(0, text.charCodeAt(2), 0));
assert.isTrue(
app.inputContext.sendKey.getCall(3).calledWith(0, text.charCodeAt(3), 0));
assert.isTrue(
app.inputContext.sendKey.getCall(4).calledWith(0, text.charCodeAt(4), 0));
assert.isTrue(
app.inputContext.sendKey.getCall(5).calledWith(0, text.charCodeAt(5), 0));
});
test('setLayoutPage', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
layoutManager: {
PAGE_INDEX_DEFAULT: 0
},
setLayoutPage: this.sinon.stub(),
inputContext: {}
};
var pageIndex = 0;
glue.init(app, 'foo');
glue.setLayoutPage(pageIndex);
assert.isTrue(app.setLayoutPage.calledWith(pageIndex));
});
test('setUpperCase', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
upperCaseStateManager: {
switchUpperCaseState: this.sinon.stub()
},
inputContext: {}
};
var state = {
isUpperCase: true,
isUpperCaseLocked: false
};
glue.init(app, 'foo');
glue.setUpperCase(state);
assert.isTrue(
app.upperCaseStateManager.switchUpperCaseState.calledWith(state));
});
test('replaceSurroundingText', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
replaceSurroundingText: this.sinon.stub()
}
};
var p = Promise.resolve();
app.inputContext.replaceSurroundingText.returns(p);
var text = 'foobar';
var offset = 3;
var length = 1;
glue.init(app, 'foo');
var returned = glue.replaceSurroundingText(text, offset, length);
assert.isTrue(
app.inputContext.replaceSurroundingText.calledWith(text, offset, length));
assert.equal(returned, p);
});
test('getNumberOfCandidatesPerRow', function() {
var glue = new InputMethodGlue();
var app = {
console: this.sinon.stub(KeyboardConsole.prototype),
viewManager: {
getNumberOfCandidatesPerRow: this.sinon.stub()
}
};
app.viewManager.getNumberOfCandidatesPerRow.returns(123);
glue.init(app, 'foo');
var row = glue.getNumberOfCandidatesPerRow();
assert.equal(row, 123);
});
test('getData', function() {
var glue = new InputMethodGlue();
var p = { stub: 'promise' };
var app = {
inputMethodDatabaseLoader: {
load: this.sinon.stub().returns(p)
}
};
glue.init(app, 'foo');
var result = glue.getData('bar');
assert.isTrue(
app.inputMethodDatabaseLoader.load.calledWithExactly('foo', 'bar'));
assert.equal(result, p);
});
});
suite('InputMethodLoader', function() {
var realInputMethods;
suiteSetup(function() {
realInputMethods = window.InputMethods;
});
suiteTeardown(function() {
window.InputMethods = realInputMethods;
});
test('start', function(done) {
var initStub = this.sinon.stub();
window.InputMethods = {
'preloaded': {
init: initStub
}
};
var loader = new InputMethodLoader({});
loader.start();
assert.equal(!!window.InputMethods.preloaded, false, 'original removed');
assert.isTrue(!!loader.getInputMethod('preloaded'), 'preloaded loaded');
assert.equal(initStub.getCall(0).args[0].app, loader.app,
'init with a glue object with a correct app');
var p = loader.getInputMethodAsync('preloaded');
p.then(function(imEngine) {
assert.isTrue(true, 'loaded');
assert.equal(imEngine, loader.getInputMethod('preloaded'));
done();
}, function() {
assert.isTrue(false, 'should not reject');
done();
});
});
test('getInputMethodAsync', function(done) {
window.InputMethods = {};
var loader = new InputMethodLoader({});
loader.SOURCE_DIR = './fake-imes/';
loader.start();
var p = loader.getInputMethodAsync('foo');
p.then(function(imEngine) {
assert.isTrue(true, 'loaded');
assert.isTrue(!!loader.getInputMethod('foo'), 'foo loaded');
assert.equal(imEngine, loader.getInputMethod('foo'));
done();
}, function() {
assert.isTrue(false, 'should not reject');
done();
});
});
test('getInputMethodAsync (twice)', function(done) {
window.InputMethods = {};
var loader = new InputMethodLoader({});
loader.SOURCE_DIR = './fake-imes/';
loader.start();
var p = loader.getInputMethodAsync('foo');
p.then(function(imEngine) {
assert.isTrue(true, 'loaded');
assert.isTrue(!!loader.getInputMethod('foo'), 'foo loaded');
assert.equal(imEngine, loader.getInputMethod('foo'));
var p2 = loader.getInputMethodAsync('foo');
assert.equal(p2, p,
'Should return the same promise without creating a new one');
p.then(function(imEngine) {
assert.isTrue(true, 'loaded');
assert.equal(imEngine, loader.getInputMethod('foo'));
done();
}, function() {
assert.isTrue(false, 'should not reject');
done();
});
}, function() {
assert.isTrue(false, 'should not reject');
done();
});
});
test('getInputMethodAsync (failed)', function(done) {
window.InputMethods = {};
var loader = new InputMethodLoader({});
loader.SOURCE_DIR = './fake-imes/';
loader.start();
var p = loader.getInputMethodAsync('bar');
p.then(function() {
assert.isTrue(false, 'should not resolve');
done();
}, function() {
assert.isTrue(true, 'rejected');
done();
});
});
test('getInputMethodAsync (twice)', function(done) {
window.InputMethods = {};
var loader = new InputMethodLoader({});
loader.SOURCE_DIR = './fake-imes/';
loader.start();
var p = loader.getInputMethodAsync('foo');
var p2 = loader.getInputMethodAsync('foo');
assert.equal(p, p2, 'Return same promise instance for the same IMEngine.');
p.then(function() {
assert.isTrue(true, 'loaded');
assert.isTrue(!!loader.getInputMethod('foo'), 'foo loaded');
done();
}, function() {
assert.isTrue(false, 'should not reject');
done();
});
});
});
suite('InputMethodManager', function() {
var realInputMethods;
var imEngineSettingsStub;
var app;
var initSettingsPromise;
var manager;
suiteSetup(function() {
realInputMethods = window.InputMethods;
});
suiteTeardown(function() {
window.InputMethods = realInputMethods;
});
setup(function () {
imEngineSettingsStub = this.sinon.stub(IMEngineSettings.prototype);
this.sinon.stub(window, 'IMEngineSettings').returns(imEngineSettingsStub);
initSettingsPromise = Promise.resolve({
suggestionsEnabled: true,
correctionsEnabled: true
});
imEngineSettingsStub.initSettings.returns(initSettingsPromise);
app = {
promiseManager: {},
layoutManager: {
currentPage: {
autoCorrectLanguage: 'xx-XX',
autoCorrectPunctuation: true
}
},
console: this.sinon.stub(KeyboardConsole.prototype),
inputContext: {
inputType: 'text',
inputMode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar',
textBeforeCursor: '',
textAfterCursor: '',
addEventListener: this.sinon.stub(),
removeEventListener: this.sinon.stub()
}
};
window.InputMethods = {
'default': realInputMethods['default']
};
manager = new InputMethodManager(app);
manager.start();
manager.loader.SOURCE_DIR = './fake-imes/';
assert.equal(manager.loader.app, manager.app);
assert.isTrue(!!manager.currentIMEngine, 'started with default IMEngine.');
});
test('activateIMEngine', function(done) {
var p = manager.activateIMEngine('foo');
p.then(function() {
assert.isTrue(true, 'resolved');
var imEngine = manager.loader.getInputMethod('foo');
assert.isTrue(!!imEngine, 'foo loaded');
assert.equal(manager.currentIMEngine, imEngine, 'currentIMEngine is set');
var activateStub = imEngine.activate;
assert.isTrue(activateStub.calledWithExactly('xx-XX', {
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar'
}, {
suggest: true,
correct: true,
correctPunctuation: true
}));
assert.equal(activateStub.getCall(0).thisValue,
imEngine);
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('activateIMEngine, autoCorrectPunctuation false', function(done) {
initSettingsPromise = Promise.resolve({
suggestionsEnabled: true,
correctionsEnabled: true
});
imEngineSettingsStub.initSettings.returns(initSettingsPromise);
manager.app.layoutManager.currentPage = {
autoCorrectPunctuation: false
};
var p = manager.activateIMEngine('foo');
p.then(function() {
assert.isTrue(true, 'resolved');
var imEngine = manager.loader.getInputMethod('foo');
var activateStub = imEngine.activate;
assert.deepEqual(activateStub.args[0][2], {
suggest: true,
correct: true,
correctPunctuation: false
});
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('activateIMEngine (failed loader)', function(done) {
var p = manager.activateIMEngine('bar');
p.then(function() {
assert.isTrue(false, 'should not resolve');
}, function() {
assert.isTrue(true, 'rejected');
}).then(done, done);
});
test('activateIMEngine (deactivate and reload after loaded)', function(done) {
var p = manager.activateIMEngine('foo');
p.then(function() {
assert.isTrue(true, 'resolved');
var imEngine = manager.loader.getInputMethod('foo');
assert.isTrue(!!imEngine, 'foo loaded');
assert.equal(manager.currentIMEngine, imEngine, 'currentIMEngine is set');
var activateStub = imEngine.activate;
assert.isTrue(activateStub.calledWithExactly('xx-XX', {
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar'
}, {
suggest: true,
correct: true,
correctPunctuation: true
}));
assert.equal(activateStub.getCall(0).thisValue,
imEngine);
manager.deactivateIMEngine();
var deactivateStub = imEngine.deactivate;
assert.isTrue(deactivateStub.calledOnce,
'deactivate should be called at ' +
'InputMethodManager#deactivateIMEngine()');
assert.equal(deactivateStub.getCall(0).thisValue,
imEngine);
var p2 = manager.activateIMEngine('foo');
assert.equal(manager.currentIMEngine,
manager.loader.getInputMethod('default'),
'currentIMEngine is set to default');
return p2;
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'should not reject');
}).then(function() {
assert.isTrue(true, 'resolved');
var imEngine = manager.loader.getInputMethod('foo');
assert.isTrue(!!imEngine, 'foo loaded');
assert.equal(manager.currentIMEngine, imEngine,
'currentIMEngine is set');
var activateStub = imEngine.activate;
assert.isTrue(activateStub.calledTwice);
assert.isTrue(activateStub.getCall(1).calledWithExactly('xx-XX', {
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar'
}, {
suggest: true,
correct: true,
correctPunctuation: true
}));
assert.equal(activateStub.getCall(1).thisValue,
imEngine);
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('selectionchange', function(done) {
var p = manager.activateIMEngine('foo');
p.then(function() {
assert.isTrue(app.inputContext.addEventListener.calledTwice);
manager.handleEvent({
type: 'selectionchange',
target: app.inputContext,
detail: {
selectionStart: 0,
selectionEnd: 0,
ownAction: true
}
});
}, function() {
assert.isTrue(false, 'should not reject');
}).then(function() {
var imEngine = manager.loader.getInputMethod('foo');
assert.isTrue(imEngine.stateChange.calledWithExactly({
ownAction: true,
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar'
}));
}).then(done, done);
});
test('surroundingtextchange', function(done) {
var p = manager.activateIMEngine('foo');
p.then(function() {
assert.isTrue(app.inputContext.addEventListener.calledTwice);
manager.handleEvent({
type: 'surroundingtextchange',
target: app.inputContext,
detail: {
text: '',
textBeforeCursor: '',
textAfterCursor: '',
ownAction: true
}
});
}, function() {
assert.isTrue(false, 'should not reject');
}).then(function() {
var imEngine = manager.loader.getInputMethod('foo');
assert.isTrue(imEngine.stateChange.calledWithExactly({
ownAction: true,
type: 'text',
inputmode: '',
selectionStart: 0,
selectionEnd: 0,
text: 'foobar'
}));
}).then(done, done);
});
});

View file

@ -0,0 +1,313 @@
'use strict';
/* global KeyboardApp, KeyboardConsole, InputMethodManager,
InputMethodDatabaseLoader, LayoutManager, SettingsPromiseManager,
L10nLoader, TargetHandlersManager, FeedbackManager,
VisualHighlightManager, CandidatePanelManager, UpperCaseStateManager,
LayoutRenderingManager, StateManager,
MockInputMethodManager, HandwritingPadsManager, ViewManager */
require('/js/keyboard/console.js');
require('/js/keyboard/input_method_manager.js');
require('/js/keyboard/input_method_database_loader.js');
require('/js/keyboard/layout_manager.js');
require('/js/keyboard/settings.js');
require('/js/keyboard/l10n_loader.js');
require('/js/keyboard/target_handlers_manager.js');
require('/js/keyboard/handwriting_pads_manager.js');
require('/js/keyboard/feedback_manager.js');
require('/js/keyboard/visual_highlight_manager.js');
require('/js/keyboard/candidate_panel_manager.js');
require('/js/keyboard/upper_case_state_manager.js');
require('/js/keyboard/layout_rendering_manager.js');
require('/js/keyboard/state_manager.js');
require('/js/keyboard/view_manager.js');
requireApp('keyboard/shared/test/unit/mocks/mock_event_target.js');
requireApp('keyboard/shared/test/unit/mocks/mock_navigator_input_method.js');
require('/js/keyboard/keyboard_app.js');
suite('KeyboardApp', function() {
var consoleStub;
var inputMethodManagerStub;
var inputMethodDatabaseLoaderStub;
var layoutManagerStub;
var settingsPromiseManagerStub;
var l10nLoaderStub;
var targetHandlersManagerStub;
var handwritingPadsManagerStub;
var feedbackManagerStub;
var visualHighlightManagerStub;
var candidatePanelManagerStub;
var upperCaseStateManagerStub;
var layoutRenderingManagerStub;
var stateManagerStub;
var viewManagerStub;
var app;
var realMozInputMethod;
setup(function() {
realMozInputMethod = navigator.mozInputMethod;
navigator.mozInputMethod = {
mgmt: this.sinon.stub(MockInputMethodManager.prototype)
};
consoleStub = this.sinon.stub(KeyboardConsole.prototype);
this.sinon.stub(window, 'KeyboardConsole').returns(consoleStub);
inputMethodDatabaseLoaderStub =
this.sinon.stub(InputMethodDatabaseLoader.prototype);
this.sinon.stub(window, 'InputMethodDatabaseLoader')
.returns(inputMethodDatabaseLoaderStub);
inputMethodManagerStub = this.sinon.stub(InputMethodManager.prototype);
this.sinon.stub(window, 'InputMethodManager')
.returns(inputMethodManagerStub);
layoutManagerStub = this.sinon.stub(LayoutManager.prototype);
this.sinon.stub(window, 'LayoutManager').returns(layoutManagerStub);
settingsPromiseManagerStub =
this.sinon.stub(SettingsPromiseManager.prototype);
this.sinon.stub(window, 'SettingsPromiseManager')
.returns(settingsPromiseManagerStub);
l10nLoaderStub = this.sinon.stub(L10nLoader.prototype);
this.sinon.stub(window, 'L10nLoader').returns(l10nLoaderStub);
targetHandlersManagerStub =
this.sinon.stub(TargetHandlersManager.prototype);
this.sinon.stub(window, 'TargetHandlersManager')
.returns(targetHandlersManagerStub);
handwritingPadsManagerStub =
this.sinon.stub(HandwritingPadsManager.prototype);
this.sinon.stub(window, 'HandwritingPadsManager')
.returns(handwritingPadsManagerStub);
feedbackManagerStub = this.sinon.stub(FeedbackManager.prototype);
this.sinon.stub(window, 'FeedbackManager').returns(feedbackManagerStub);
visualHighlightManagerStub =
this.sinon.stub(VisualHighlightManager.prototype);
this.sinon.stub(window, 'VisualHighlightManager')
.returns(visualHighlightManagerStub);
candidatePanelManagerStub =
this.sinon.stub(CandidatePanelManager.prototype);
this.sinon.stub(window, 'CandidatePanelManager')
.returns(candidatePanelManagerStub);
upperCaseStateManagerStub =
this.sinon.stub(UpperCaseStateManager.prototype);
this.sinon.stub(window, 'UpperCaseStateManager')
.returns(upperCaseStateManagerStub);
layoutRenderingManagerStub =
this.sinon.stub(LayoutRenderingManager.prototype);
this.sinon.stub(window, 'LayoutRenderingManager')
.returns(layoutRenderingManagerStub);
stateManagerStub =
this.sinon.stub(StateManager.prototype);
this.sinon.stub(window, 'StateManager')
.returns(stateManagerStub);
viewManagerStub =
this.sinon.stub(ViewManager.prototype);
this.sinon.stub(window, 'ViewManager')
.returns(viewManagerStub);
window.requestAnimationFrame = this.sinon.stub();
app = new KeyboardApp();
app.start();
assert.isTrue(window.KeyboardConsole.calledWithNew());
assert.isTrue(window.InputMethodManager.calledWithNew());
assert.isTrue(window.InputMethodDatabaseLoader.calledWithNew());
assert.isTrue(window.LayoutManager.calledWithNew());
assert.isTrue(window.SettingsPromiseManager.calledWithNew());
assert.isTrue(window.L10nLoader.calledWithNew());
assert.isTrue(window.TargetHandlersManager.calledWithNew());
assert.isTrue(window.FeedbackManager.calledWithNew());
assert.isTrue(window.VisualHighlightManager.calledWithNew());
assert.isTrue(window.CandidatePanelManager.calledWithNew());
assert.isTrue(window.UpperCaseStateManager.calledWithNew());
assert.isTrue(window.LayoutRenderingManager.calledWithNew());
assert.isTrue(window.StateManager.calledWithNew());
assert.isTrue(window.ViewManager.calledWithNew());
assert.isTrue(window.InputMethodManager.calledWith(app));
assert.isTrue(window.InputMethodDatabaseLoader.calledWith(app));
assert.isTrue(window.LayoutManager.calledWith(app));
assert.isTrue(window.TargetHandlersManager.calledWith(app));
assert.isTrue(window.FeedbackManager.calledWith(app));
assert.isTrue(window.VisualHighlightManager.calledWith(app));
assert.isTrue(window.CandidatePanelManager.calledWith(app));
assert.isTrue(window.ViewManager.calledWith(app));
assert.isTrue(consoleStub.start.calledOnce);
assert.isTrue(inputMethodManagerStub.start.calledOnce);
assert.isTrue(inputMethodDatabaseLoaderStub.start.calledOnce);
assert.isTrue(layoutManagerStub.start.calledOnce);
assert.isTrue(targetHandlersManagerStub.start.calledOnce);
assert.isTrue(handwritingPadsManagerStub.start.calledOnce);
assert.isTrue(feedbackManagerStub.start.calledOnce);
assert.isTrue(visualHighlightManagerStub.start.calledOnce);
assert.isTrue(candidatePanelManagerStub.start.calledOnce);
assert.isTrue(upperCaseStateManagerStub.start.calledOnce);
assert.isTrue(layoutRenderingManagerStub.start.calledOnce);
assert.isTrue(stateManagerStub.start.calledOnce);
assert.isTrue(viewManagerStub.start.calledOnce);
});
teardown(function() {
app.stop();
assert.isTrue(inputMethodDatabaseLoaderStub.stop.calledOnce);
assert.isTrue(targetHandlersManagerStub.stop.calledOnce);
assert.isTrue(handwritingPadsManagerStub.stop.calledOnce);
assert.isTrue(feedbackManagerStub.stop.calledOnce);
assert.isTrue(visualHighlightManagerStub.stop.calledOnce);
assert.isTrue(candidatePanelManagerStub.stop.calledOnce);
assert.isTrue(upperCaseStateManagerStub.stop.calledOnce);
assert.isTrue(layoutRenderingManagerStub.stop.calledOnce);
assert.isTrue(stateManagerStub.stop.calledOnce);
assert.isTrue(viewManagerStub.stop.calledOnce);
navigator.mozInputMethod = realMozInputMethod;
});
test('getContainer', function() {
var el = {};
this.sinon.stub(document, 'getElementById').returns(el);
var result = app.getContainer();
assert.isTrue(
document.getElementById.calledWith(app.CONATINER_ELEMENT_ID));
assert.equal(result, el);
});
test('setInputContext', function() {
var inputContext = {};
app.setInputContext(inputContext);
assert.equal(app.inputContext, inputContext);
});
suite('getBasicInputType', function() {
test('without inputContext', function() {
var result = app.getBasicInputType();
assert.equal(result, 'text');
});
test('inputType = url', function() {
app.inputContext = {
inputType: 'url'
};
var result = app.getBasicInputType();
assert.equal(result, 'url');
});
test('inputType = tel', function() {
app.inputContext = {
inputType: 'tel'
};
var result = app.getBasicInputType();
assert.equal(result, 'tel');
});
test('inputType = email', function() {
app.inputContext = {
inputType: 'email'
};
var result = app.getBasicInputType();
assert.equal(result, 'email');
});
test('inputType = text', function() {
app.inputContext = {
inputType: 'text'
};
var result = app.getBasicInputType();
assert.equal(result, 'text');
});
test('inputType = password', function() {
app.inputContext = {
inputType: 'password'
};
var result = app.getBasicInputType();
assert.equal(result, 'text');
});
test('inputType = search', function() {
app.inputContext = {
inputType: 'search'
};
var result = app.getBasicInputType();
assert.equal(result, 'search');
});
test('inputType = foo', function() {
app.inputContext = {
inputType: 'foo'
};
var result = app.getBasicInputType();
assert.equal(result, 'text');
});
test('inputType = number', function() {
app.inputContext = {
inputType: 'number'
};
var result = app.getBasicInputType();
assert.equal(result, 'number');
});
test('inputType = range', function() {
app.inputContext = {
inputType: 'range'
};
var result = app.getBasicInputType();
assert.equal(result, 'number');
});
});
test('supportsSwitching', function() {
navigator.mozInputMethod.mgmt.supportsSwitching.returns(true);
var result = app.supportsSwitching();
assert.isTrue(navigator.mozInputMethod.mgmt.supportsSwitching.calledOnce);
assert.isTrue(result);
});
test('setLayoutPage', function() {
app.inputMethodManager.currentIMEngine = {
setLayoutPage: this.sinon.stub()
};
app.layoutManager.currentPageIndex = 42;
app.setLayoutPage(12);
assert.isTrue(
app.layoutManager.updateLayoutPage.calledWith(12));
assert.isTrue(
app.layoutRenderingManager.updateLayoutRendering.calledOnce);
assert.isTrue(
app.inputMethodManager.currentIMEngine.setLayoutPage.calledWith(42));
});
});

View file

@ -0,0 +1,29 @@
'use strict';
/* global L10nLoader */
require('/js/keyboard/l10n_loader.js');
suite('L10nLoader', function() {
test('load() should load the script', function() {
var appendChildSpy = this.sinon.spy(document.body, 'appendChild');
var loader = new L10nLoader();
loader.SCRIPT_URL = encodeURI('data:text/javascript,');
loader.load();
assert.equal(appendChildSpy.getCall(0).args[0].src, loader.SCRIPT_URL);
});
test('load() should load the script once when called twice', function() {
var appendChildSpy = this.sinon.spy(document.body, 'appendChild');
var loader = new L10nLoader();
loader.SCRIPT_URL = encodeURI('data:text/javascript,');
loader.load();
loader.load();
assert.equal(appendChildSpy.callCount, 1);
});
});

View file

@ -0,0 +1,217 @@
'use strict';
/* global LayoutLoader, LayoutNormalizer */
require('/js/keyboard/layout_loader.js');
require('/js/keyboard/layout_normalizer.js');
suite('LayoutLoader', function() {
var realKeyboards;
var expectedFooLayout = {
keys: [
[
{ value: 'foo' }
]
]
};
var expectedFoo2Layout = {
keys: [
[
{ value: 'foo2' }
]
]
};
var expectedFoo3Layout = {
keys: [
[
{ value: 'foo3' }
]
]
};
suiteSetup(function() {
realKeyboards = window.Keyboards;
sinon.stub(LayoutNormalizer.prototype, 'normalize', function(){
this._normalizedLayout = this._layout;
});
});
suiteTeardown(function() {
window.Keyboards = realKeyboards;
});
test('start', function(done) {
window.Keyboards = {
'preloaded': {
keys: [
[
{ value: 'preloaded' }
]
]
}
};
var loader = new LayoutLoader();
loader.start();
assert.equal(!!window.Keyboards.preloaded, false, 'original removed');
assert.isTrue(!!loader.getLayout('preloaded'), 'preloaded loaded');
var p = loader.getLayoutAsync('preloaded');
p.then(function(layout) {
assert.isTrue(true, 'loaded');
// the loader instantiates normalizer by using its getLayout
// so testing this would test getLayout at the same time
// (same for following tests)
assert.deepEqual(
loader._initializedLayouts.preloaded,
{
keys: [
[
{ value: 'preloaded' }
]
]
},
'normalizer argument "layout" incorrect'
);
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('getLayoutAsync', function(done) {
window.Keyboards = {};
var loader = new LayoutLoader();
loader.SOURCE_DIR = './fake-layouts/';
loader.start();
var p = loader.getLayoutAsync('foo');
p.then(function(layout) {
assert.deepEqual(
loader._initializedLayouts.foo,
expectedFooLayout,
'normalizer argument "layout" incorrect'
);
assert.isTrue(true, 'loaded');
assert.equal(layout, loader.getLayout('foo'));
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('getLayoutAsync (init two layouts)', function(done) {
window.Keyboards = {};
var loader = new LayoutLoader();
loader.SOURCE_DIR = './fake-layouts/';
loader.start();
var p = loader.getLayoutAsync('foo2');
p.then(function(layout) {
assert.isTrue(true, 'loaded');
assert.deepEqual(
loader._initializedLayouts.foo2,
expectedFoo2Layout,
'normalizer argument "layout" incorrect'
);
assert.deepEqual(
loader._initializedLayouts.foo3,
expectedFoo3Layout,
'normalizer argument "layout" incorrect'
);
assert.deepEqual(window.Keyboards, {});
var p2 = loader.getLayoutAsync('foo3');
return p2;
}).then(function(layout) {
assert.equal(layout, loader.getLayout('foo3'));
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('getLayoutAsync (twice after first one)', function(done) {
window.Keyboards = {};
var loader = new LayoutLoader();
loader.SOURCE_DIR = './fake-layouts/';
loader.start();
var p = loader.getLayoutAsync('foo');
p.then(function(layout) {
assert.isTrue(true, 'loaded');
assert.deepEqual(
loader.getLayout('foo'), expectedFooLayout, 'foo loaded');
assert.equal(layout, loader.getLayout('foo'));
var p2 = loader.getLayoutAsync('foo');
assert.equal(p2, p,
'Should return the same promise without creating a new one');
return p2;
}).then(function(layout) {
assert.isTrue(true, 'loaded');
assert.equal(layout, loader.getLayout('foo'));
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
test('getLayoutAsync (failed)', function(done) {
window.Keyboards = {};
var loader = new LayoutLoader();
loader.SOURCE_DIR = './fake-layouts/';
loader.start();
var p = loader.getLayoutAsync('bar');
p.then(function() {
assert.isTrue(false, 'should not resolve');
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(true, 'rejected');
}).then(done, done);
});
test('getLayoutAsync (twice at the same time)', function(done) {
window.Keyboards = {};
var loader = new LayoutLoader();
loader.SOURCE_DIR = './fake-layouts/';
loader.start();
var p = loader.getLayoutAsync('foo');
var p2 = loader.getLayoutAsync('foo');
assert.equal(p, p2, 'Return same promise instance for the same layout.');
p.then(function() {
assert.isTrue(true, 'loaded');
assert.deepEqual(
loader.getLayout('foo'), expectedFooLayout, 'foo loaded');
}, function(e) {
if (e) {
throw (e);
}
assert.isTrue(false, 'should not reject');
}).then(done, done);
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,681 @@
'use strict';
/* global LayoutNormalizer, LayoutKeyNormalizer, KeyEvent */
require('/js/keyboard/layout_normalizer.js');
// little helper to build an array filled with numbers from |from| to |to|.
// ex: range(1, 10) -> [1,2,3,...9]
// useful for looping a specific number of iterations
var range = (from, to) => {
if (undefined === to) {
to = from;
from = 0;
}
var rangeGenerator = function *() {
for(var i = from; i < to; i++){
yield i;
}
};
return Array.from(rangeGenerator());
};
suite('LayoutKeyNormalizer', function() {
var SPECIAL_CODES_MAP = {
'VK_BACK_SPACE': KeyEvent.DOM_VK_BACK_SPACE,
'VK_CAPS_LOCK': KeyEvent.DOM_VK_CAPS_LOCK,
'VK_RETURN': KeyEvent.DOM_VK_RETURN,
'VK_VK_ALT': KeyEvent.DOM_VK_ALT
};
suite('isSpecialKey', function() {
test('keyCode is in special codes', function(){
var normalizer = new LayoutKeyNormalizer();
Object.keys(SPECIAL_CODES_MAP).forEach(keyCodeName => {
var key = {
keyCode: SPECIAL_CODES_MAP[keyCodeName]
};
assert.isTrue(normalizer._isSpecialKey(key),
keyCodeName + ' should be regarded as special key.');
});
});
test('keyCode is <= 0', function(){
var normalizer = new LayoutKeyNormalizer();
// for the sake of this test, we only test 0 to -128
range(-128, 1).forEach(keyCode => {
var key = {
keyCode: keyCode
};
assert.isTrue(normalizer._isSpecialKey(key),
keyCode + ' should be regarded as special key.');
});
});
test('keyCode is > 0 and is not in special codes', function(){
var normalizer = new LayoutKeyNormalizer();
// for the sake of this test, we only test 1 to 255
var keyCodeName; // linter needs this definition
var specialKeyCodes = [for (keyCodeName of Object.keys(SPECIAL_CODES_MAP))
SPECIAL_CODES_MAP[keyCodeName]];
range(1, 256).forEach(keyCode => {
if (specialKeyCodes.indexOf(keyCode) !== -1) {
return;
}
var key = {
keyCode: keyCode
};
assert.isFalse(normalizer._isSpecialKey(key),
keyCode + ' should not be regarded as special key.');
});
});
});
suite('isButton', function() {
// As per bug#1212588, some special keys are to be regarded as
// buttons (as per accesibility programs), but not all of
// them. This unit test makes sure the functionality exists in the
// layout normalizer. In case different keys are later decided to
// swap roles, please change this test so that it stays
// meaningful.
var key = {
keyCode: KeyEvent.DOM_VK_ALT,
value: 'alt'
};
var button = {
keyCode: KeyEvent.DOM_VK_RETURN,
value: 'enter'
};
test('Alt is a key', function(){
var normalizer = new LayoutKeyNormalizer();
assert.isFalse(normalizer._isButton(key),
'alt should be regarded as a role=key, not a button.');
});
test('Return is a button', function(){
var normalizer = new LayoutKeyNormalizer();
assert.isTrue(normalizer._isButton(button),
'return should be regarded as a role=button, not a key.');
});
test('isButton present on normalized key', function(){
var normalizer = new LayoutNormalizer({
keys: [
[{value: 'a'}]
],
upperCase: {'a': 'C'}
});
normalizer.normalize();
assert.isTrue(
normalizer._layout.pages[0].keys[0][0].hasOwnProperty('isButton'),
'a synthetic key made from a simple keyboard layout ' +
'should have property isButton');
});
});
suite('getUpperCaseValue', function() {
// For space key, special key, and composite key tests,
// we're deliberately not passing layout,
// as if the normalizer needs to use layout in the function, then something
// is definitely wrong.
test('Space key', function(){
var normalizer = new LayoutKeyNormalizer();
var key = {
keyCode: KeyEvent.DOM_VK_SPACE,
value: 'space'
};
assert.equal(normalizer._getUpperCaseValue(key), 'space');
});
test('Special keys', function(){
var normalizer = new LayoutKeyNormalizer();
Object.keys(SPECIAL_CODES_MAP).forEach(keyCodeName => {
var key = {
keyCode: SPECIAL_CODES_MAP[keyCodeName],
value: keyCodeName
};
assert.equal(normalizer._getUpperCaseValue(key), keyCodeName);
});
});
test('Composite keys', function(){
var normalizer = new LayoutKeyNormalizer();
var key = {
keyCode: 'C'.charCodeAt(0),
value: 'Com',
compositeKey: 'Com'
};
assert.equal(normalizer._getUpperCaseValue(key), 'Com');
});
test('Use uppercase from layout', function(){
var normalizer = new LayoutKeyNormalizer({
upperCase: {
'a': 'E'
}
});
var key = {
keyCode: 'a'.charCodeAt(0),
value: 'a'
};
assert.equal(
normalizer._getUpperCaseValue(key), 'E',
'upperCase of "a" should be "E" in this crafted test');
});
test('Use key.value.toUpperCase()', function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
keyCode: 'a'.charCodeAt(0),
value: 'a'
};
assert.equal(normalizer._getUpperCaseValue(key), 'A');
});
});
suite('normalizeKey', function() {
test('Calling getUpperCaseValue', function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a'
};
var stubGetUpperCaseValue =
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.isTrue(stubGetUpperCaseValue.calledWith(key));
});
test('keyCode is not present: value should derive into keyCode and ' +
'keyCodeUpper', function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a'
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.equal(key.keyCode, 'a'.charCodeAt(0));
assert.equal(key.keyCodeUpper, 'A'.charCodeAt(0));
});
test('keyCode is present: should derive into keyCodeUpper', function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a',
keyCode: 'e'.charCodeAt(0)
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.equal(key.keyCodeUpper, 'e'.charCodeAt(0),
'keyCodeUpper should be keyCode of "e" ' +
'in this crafted test');
});
test('value should derive into lowercaseValue and uppercaseValue',
function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a'
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.equal(key.lowercaseValue, 'a');
assert.equal(key.uppercaseValue, 'A');
});
test('isSpecialKey', function(){
var normalizer = new LayoutKeyNormalizer({});
var stubIsSpecialKey = this.sinon.stub(normalizer, '_isSpecialKey');
stubIsSpecialKey.returns(true);
var key = {
value: 'a'
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.isTrue(key.isSpecialKey);
stubIsSpecialKey.returns(false);
var key2 = {
value: 'a'
};
normalizer.normalizeKey(key2);
assert.isFalse(key2.isSpecialKey);
});
test('longPressKeyCode is not present: should derive from longPressValue',
function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a',
longPressValue: 'a'
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.equal(key.longPressKeyCode, 'a'.charCodeAt(0));
});
test('longPressKeyCode is present: should not derive from longPressValue',
function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'a',
longPressValue: 'a',
longPressKeyCode: 'b'.charCodeAt(0)
};
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
normalizer.normalizeKey(key);
assert.equal(key.longPressKeyCode, 'b'.charCodeAt(0),
'longPressKeyCode should be keyCode of "b" ' +
'in this crafted test');
});
test('supprtsSwitching key should be normalized too', function(){
var normalizer = new LayoutKeyNormalizer({});
var key = {
value: 'c',
supportsSwitching: {
value: 'a'
}
};
this.sinon.stub(normalizer, '_getUpperCaseValue')
.onFirstCall().returns('C')
.onSecondCall().returns('A');
normalizer.normalizeKey(key);
assert.equal(key.supportsSwitching.keyCodeUpper, 'A'.charCodeAt(0));
});
test('normalize a key which has alternative keys', function() {
var normalizer = new LayoutKeyNormalizer({});
this.sinon.stub(normalizer, '_getUpperCaseValue').returns('A');
var key = {
value: 'a'
};
normalizer.normalizeKey(key, {}, true);
assert.equal(key.className, 'alternate-indicator');
});
});
});
suite('LayoutNormalizer', function() {
suite('normalizePageKeys', function() {
var stubNormalizeKey;
setup(function() {
stubNormalizeKey =
sinon.stub(LayoutKeyNormalizer.prototype, 'normalizeKey');
});
teardown(function() {
stubNormalizeKey.restore();
});
test('keys', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[ {value: 'a'}, {value: 'b'} ],
[ {value: 'c'}, {value: 'd'}, {value: 'e'} ]
]
};
normalizer._normalizePageKeys(page);
assert.isTrue(stubNormalizeKey.calledWith({value: 'a'}));
assert.isTrue(stubNormalizeKey.calledWith({value: 'b'}));
assert.isTrue(stubNormalizeKey.calledWith({value: 'c'}));
assert.isTrue(stubNormalizeKey.calledWith({value: 'd'}));
assert.isTrue(stubNormalizeKey.calledWith({value: 'e'}));
});
test('textLayoutOverwrite', function() {
var normalizer = new LayoutNormalizer({});
var page = {
textLayoutOverwrite: {
'a': false,
'b': 'B'
}
};
stubNormalizeKey.returns('C');
normalizer._normalizePageKeys(page);
assert.isFalse(stubNormalizeKey.calledWith({value: false}));
assert.isTrue(stubNormalizeKey.calledWith({value: 'B'}));
assert.deepEqual(page.textLayoutOverwrite, {
'a': false,
'b': 'C'
});
});
test('overwriting key in textLayoutOverwrite has alternative keys',
function() {
var normalizer = new LayoutNormalizer({});
var page = {
textLayoutOverwrite: {
'a': 'b'
},
alt: {
'b': 'cdefg'
}
};
// we'd like to have normalizeKey really do it job in this test
stubNormalizeKey.restore();
var overwritingkey = 'b';
var hasAlternativeKeys = normalizer._hasAlternativeKeys(overwritingkey,
page);
assert.isTrue(hasAlternativeKeys);
normalizer._normalizePageKeys(page);
assert.equal(page.textLayoutOverwrite.a.className,
'alternate-indicator');
});
});
suite('normalizePageAltKeys', function() {
test('normalize alt menu (single char keys)', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: {
'a': 'áàâäåãāæ'
},
upperCase: {}
};
normalizer._normalizePageAltKeys(page);
assert.deepEqual(page, {
keys: [
[
{ value: 'preloaded' }
]
],
alt: { 'a': [ 'á', 'à', 'â', 'ä', 'å', 'ã', 'ā', 'æ' ],
'A': [ 'Á', 'À', 'Â', 'Ä', 'Å', 'Ã', 'Ā', 'Æ' ] },
upperCase: {}
});
});
test('normalize alt menu (with multi-char keys)', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: {
'a': 'á à â A$'
},
upperCase: {}
};
normalizer._normalizePageAltKeys(page);
assert.deepEqual(page, {
keys: [
[
{ value: 'preloaded' }
]
],
alt: { 'a': [ 'á', 'à', 'â', 'A$' ],
'A': [ 'Á', 'À', 'Â', 'A$' ] },
upperCase: {}
});
});
test('normalize alt menu (with one multi-char keys)', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: {
'a': 'A$ '
},
upperCase: {}
};
normalizer._normalizePageAltKeys(page);
assert.deepEqual(page, {
keys: [
[
{ value: 'preloaded' }
]
],
alt: { 'a': [ 'A$' ],
'A': [ 'A$' ] },
upperCase: {}
});
});
test('normalize alt menu (with Turkish \'i\' key)', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: {
'i': 'ß'
},
upperCase: {
'i': 'İ'
}
};
normalizer._normalizePageAltKeys(page);
assert.deepEqual(page, {
keys: [
[
{ value: 'preloaded' }
]
],
alt: { 'i': [ 'ß' ],
'İ': [ 'ß' ] },
upperCase: {
'i': 'İ'
}
});
});
test('normalize alt menu (with Catalan \'l·l\' key)', function() {
var normalizer = new LayoutNormalizer({});
var page = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: {
'l': 'l·l ł £'
},
upperCase: {}
};
normalizer._normalizePageAltKeys(page);
var expectedPage = {
keys: [
[
{ value: 'preloaded' }
]
],
alt: { 'l': [ 'l·l', 'ł', '£' ],
'L': [ 'L·l', 'Ł', '£' ] },
upperCase: {}
};
expectedPage.alt.L.upperCaseLocked = [ 'L·L', 'Ł', '£' ];
assert.deepEqual(page, expectedPage);
});
});
suite('normalize', function() {
test('generation of page 0', function(){
var normalizer = new LayoutNormalizer({
alt: {'alt': [1]},
keys: [2],
upperCase: {'a': 'A'},
width: 3,
keyClassName: 'C',
typeInsensitive: true,
textLayoutOverwrite: {',': 'comma'},
needsCommaKey: true,
secondLayout: true,
specificCssRule: true
});
this.sinon.stub(normalizer, '_normalizePageKeys');
this.sinon.stub(normalizer, '_normalizePageAltKeys');
normalizer.normalize();
assert.deepEqual(normalizer._layout, {
pages: [
{
alt: {'alt': [1]},
keys: [2],
upperCase: {'a': 'A'},
width: 3,
keyClassName: 'C',
typeInsensitive: true,
textLayoutOverwrite: {',': 'comma'},
needsCommaKey: true,
secondLayout: true,
specificCssRule: true
}
]
}, 'page 0 of layout was not generated from layout top-most attributes');
});
test('each page gets normalized', function(){
var layout = {
pages: []
};
range(4).forEach( index => {
layout.pages.push({index: index});
});
var normalizer = new LayoutNormalizer(layout);
var stubNormalizePageKeys =
this.sinon.stub(normalizer, '_normalizePageKeys');
var stubNormalizePageAltKeys =
this.sinon.stub(normalizer, '_normalizePageAltKeys');
normalizer.normalize();
range(4).forEach( index => {
assert.isTrue(
stubNormalizePageKeys.calledWith({index: index}),
'normalizePageKeys was not called for index = ' + index
);
assert.isTrue(
stubNormalizePageAltKeys.calledWith({index: index}),
'normalizePageAltKeys was not called for index = ' + index
);
});
});
});
});
suite('Layout normalization: Cross-module integrated tests', function() {
test('getUpperCase correctly uses moved-to-page0 properties', function(){
var normalizer = new LayoutNormalizer({
keys: [
[{value: 'a'}]
],
upperCase: {'a': 'C'}
});
normalizer.normalize();
assert.deepEqual(normalizer._layout.pages[0].keys,
[
[{
value: 'a',
keyCode: 97,
keyCodeUpper: 67,
lowercaseValue: 'a',
uppercaseValue: 'C',
isSpecialKey: false,
isButton: false
}]
], 'upperCase of "a" should be "C" in this crafted test');
});
});

View file

@ -0,0 +1,299 @@
'use strict';
/* global LayoutRenderingManager, KeyboardConsole */
require('/js/keyboard/console.js');
require('/js/keyboard/layout_rendering_manager.js');
suite('LayoutRenderingManager', function() {
var app;
var manager;
var viewManager;
setup(function() {
this.sinon.stub(window, 'setTimeout');
this.sinon.stub(window, 'clearTimeout');
this.sinon.stub(window, 'addEventListener');
this.sinon.stub(window, 'removeEventListener');
this.sinon.stub(window, 'resizeTo');
viewManager = {
render: this.sinon.stub(),
resize: this.sinon.stub(),
setUpperCaseLock: this.sinon.stub(),
showCandidates: this.sinon.stub(),
getWidth: this.sinon.stub().returns(600),
getHeight: this.sinon.stub().returns(400),
getKeyArray: this.sinon.stub().returns([]),
getKeyWidth: this.sinon.stub().returns(15),
getKeyHeight: this.sinon.stub().returns(12),
candidatePanel: {
clientHeight: 123
}
};
app = {
getBasicInputType: this.sinon.stub().returns('foo'),
console: this.sinon.stub(KeyboardConsole.prototype),
candidatePanelManager: {
currentCandidates: []
},
layoutManager: {
currentPage: {
keys: [ { value: 'currentPage' } ]
},
currentPageIndex: 0,
PAGE_INDEX_DEFAULT: 0
},
inputMethodManager: {
currentIMEngine: {
setLayoutParams: this.sinon.stub()
}
},
upperCaseStateManager: {
isUpperCase: true
},
viewManager: viewManager
};
Object.defineProperty(document, 'hidden',
{ value: false, configurable: true });
manager = new LayoutRenderingManager(app);
manager.start();
assert.isTrue(window.setTimeout.calledOnce);
window.setTimeout.getCall(0).args[0].call(window);
assert.isTrue(window.addEventListener.calledWith('resize', manager));
});
suite('updateLayoutRendering', function() {
var p;
teardown(function(done) {
viewManager.render.getCall(0).args[2].call(window);
assert.isTrue(window.resizeTo.calledWith(600, 400));
p.then(function() {
assert.equal(
viewManager.setUpperCaseLock.firstCall.args[0],
app.upperCaseStateManager);
assert.equal(viewManager.showCandidates.firstCall.args[0],
app.candidatePanelManager.currentCandidates);
assert.isTrue(
app.inputMethodManager.currentIMEngine.setLayoutParams.calledWith({
keyboardWidth: 600,
keyboardHeight: 277,
keyArray: [],
keyWidth: 15,
keyHeight: 12
}));
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'Should not reject.');
}).then(done, done);
});
test('isUpperCase = false', function() {
app.upperCaseStateManager.isUpperCase = false;
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: false,
inputType: 'foo',
showCandidatePanel: false
}));
});
test('w/o autoCorrectLanguage, w/o displaysCandidates()', function() {
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: true,
inputType: 'foo',
showCandidatePanel: false
}));
});
test('w/ autoCorrectLanguage, w/o displaysCandidates()', function() {
app.layoutManager.currentPage.autoCorrectLanguage = 'zz-ZZ';
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: true,
inputType: 'foo',
showCandidatePanel: true
}));
});
test('w/ autoCorrectLanguage, w/ displaysCandidates()', function() {
app.layoutManager.currentPage.autoCorrectLanguage = 'zz-ZZ';
app.inputMethodManager.currentIMEngine.displaysCandidates =
this.sinon.stub().returns(false);
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: true,
inputType: 'foo',
showCandidatePanel: false
}));
});
test('w/ needsCandidatePanel, w/o displaysCandidates()', function() {
app.layoutManager.currentPage.needsCandidatePanel = true;
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: true,
inputType: 'foo',
showCandidatePanel: true
}));
});
test('w/ needsCandidatePanel, w/ displaysCandidates()', function() {
app.layoutManager.currentPage.needsCandidatePanel = true;
app.inputMethodManager.currentIMEngine.displaysCandidates =
this.sinon.stub().returns(false);
p = manager.updateLayoutRendering();
assert.isTrue(viewManager.render.calledWith(
app.layoutManager.currentPage,
{
uppercase: true,
inputType: 'foo',
showCandidatePanel: false
}));
});
});
suite('Before rendering', function() {
test('resize event', function() {
var evt = {
type: 'resize',
target: window
};
manager.handleEvent(evt);
assert.isFalse(viewManager.resize.calledOnce);
assert.isFalse(
app.inputMethodManager.currentIMEngine.setLayoutParams.calledOnce);
assert.isFalse(window.resizeTo.calledOnce);
});
test('updateCandidatesRendering', function() {
manager.updateCandidatesRendering();
assert.isFalse(viewManager.showCandidates.calledOnce);
});
test('updateUpperCaseRendering', function() {
window.requestAnimationFrame = this.sinon.stub();
manager.updateUpperCaseRendering();
assert.isFalse(window.requestAnimationFrame.calledOnce);
});
});
suite('Rendered', function() {
setup(function(done) {
manager.updateLayoutRendering().then(function() {
}, function(e) {
if (e) {
throw e;
}
assert.isTrue(false, 'Should not reject.');
}).then(done, done);
viewManager.render.getCall(0).args[2].call(window);
});
test('resize event', function() {
var evt = {
type: 'resize',
target: window
};
manager.handleEvent(evt);
assert.equal(viewManager.resize.callCount, 1);
assert.isTrue(
app.inputMethodManager.currentIMEngine.setLayoutParams.calledWith({
keyboardWidth: 600,
keyboardHeight: 277,
keyArray: [],
keyWidth: 15,
keyHeight: 12
}));
assert.isTrue(window.resizeTo.calledWith(600, 400));
});
test('updateCandidatesRendering', function() {
manager.updateCandidatesRendering();
assert.isTrue(viewManager.showCandidates.calledTwice);
assert.equal(viewManager.showCandidates.secondCall.args[0],
app.candidatePanelManager.currentCandidates);
});
test('updateUpperCaseRendering', function() {
window.requestAnimationFrame = this.sinon.stub();
manager.updateUpperCaseRendering();
window.requestAnimationFrame.getCall(0).args[0].call(window);
assert.isTrue(viewManager.setUpperCaseLock.calledTwice);
assert.equal(viewManager.setUpperCaseLock.secondCall.args[0],
app.upperCaseStateManager);
});
});
suite('domObjectMap and getTargetObject', function() {
test('get object with existent key', function() {
var key = {
dummy: 'dummy'
};
var object = {
result: 'result'
};
manager.domObjectMap.set(key, object);
assert.equal(manager.getTargetObject(key), object);
});
test('null and undefined key should return empty object', function() {
assert.deepEqual(manager.getTargetObject(null), {});
assert.deepEqual(manager.getTargetObject(undefined), {});
});
test('inexistent key should return empty object instead of undefined',
function() {
var inexistentKey = {
someProp: 'dummy'
};
assert.deepEqual(manager.getTargetObject(inexistentKey), {});
});
});
});

View file

@ -0,0 +1,376 @@
'use strict';
/* global MockNavigatorMozSettings, MockNavigatorMozSettingsLock,
SettingsPromiseManager, SettingsManagerBase */
require('/js/keyboard/settings.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_dom_request.js');
require('/shared/js/input_mgmt/mock_navigator_mozsettings.js');
suite('SettingsPromiseManager', function() {
var realMozSettings;
suiteSetup(function() {
realMozSettings = navigator.mozSettings;
});
suiteTeardown(function() {
navigator.mozSettings = realMozSettings;
});
test('get one key', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.get('foo');
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledOnce);
assert.isTrue(lockGetSpy.calledWith('foo'));
var req = lockGetSpy.getCall(0).returnValue;
req.fireSuccess({ 'foo': 'bar' });
p.then(function(value) {
assert.equal(value, 'bar');
done();
}, function(error) {
assert.isTrue(false, 'Should not reject the promise.');
done();
});
});
test('get multiple keys', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.get(['foo', 'foo2']);
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledTwice);
assert.isTrue(lockGetSpy.calledWith('foo'));
assert.isTrue(lockGetSpy.calledWith('foo2'));
var req1 = lockGetSpy.getCall(0).returnValue;
req1.fireSuccess({ 'foo': 'bar' });
var req2 = lockGetSpy.getCall(1).returnValue;
req2.fireSuccess({ 'foo2': 'bar2' });
p.then(function(values) {
assert.deepEqual(values, ['bar', 'bar2']);
done();
}, function(error) {
assert.isTrue(false, 'Should not reject the promise.');
done();
});
});
test('get, handle error', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.get('foo');
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledOnce);
assert.isTrue(lockGetSpy.calledWith('foo'));
var req = lockGetSpy.getCall(0).returnValue;
req.fireError('error');
p.then(function(value) {
assert.isTrue(false, 'Should not resolve the promise.');
done();
}, function(error) {
assert.isTrue(true, 'Promise rejected.');
done();
});
});
test('set one key', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockSetSpy = this.sinon.spy(lock, 'set');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.set('foo', 'bar');
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockSetSpy.calledOnce);
assert.isTrue(lockSetSpy.calledWith({'foo': 'bar' }));
var req = lockSetSpy.getCall(0).returnValue;
req.fireSuccess(0);
p.then(function() {
assert.isTrue(true, 'set success');
done();
}, function(error) {
assert.isTrue(false, 'Should not reject the promise.');
done();
});
});
test('set multiple keys', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockSetSpy = this.sinon.spy(lock, 'set');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.set({foo: 'bar', foo2: 'bar2' });
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockSetSpy.calledOnce);
assert.isTrue(lockSetSpy.calledWith({foo: 'bar', foo2: 'bar2' }));
var req = lockSetSpy.getCall(0).returnValue;
req.fireSuccess(0);
p.then(function() {
assert.isTrue(true, 'set success');
done();
}, function(error) {
assert.isTrue(false, 'Should not reject the promise.');
done();
});
});
test('set, handle error', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockSetSpy = this.sinon.spy(lock, 'set');
createLockStub.returns(lock);
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.set('foo', 'bar');
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockSetSpy.calledOnce);
assert.isTrue(lockSetSpy.calledWith({'foo': 'bar' }));
var req = lockSetSpy.getCall(0).returnValue;
req.fireError('error');
p.then(function(value) {
assert.isTrue(false, 'Should not resolve the promise.');
done();
}, function(error) {
assert.isTrue(true, 'Promise rejected.');
done();
});
});
test('update one', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
var lockSetSpy = this.sinon.spy(lock, 'set');
createLockStub.returns(lock);
var callback = this.sinon.spy(function(val) {
return 'bar2';
});
var promiseManager = new SettingsPromiseManager();
var p = promiseManager.updateOne('foo', callback);
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledOnce);
assert.isTrue(lockGetSpy.calledWith('foo'));
var req = lockGetSpy.getCall(0).returnValue;
req.fireSuccess({ 'foo': 'bar' });
assert.isTrue(callback.calledOnce);
assert.isTrue(lockSetSpy.calledOnce);
assert.isTrue(lockSetSpy.calledWith({ 'foo': 'bar2' }));
var req2 = lockSetSpy.getCall(0).returnValue;
req2.fireSuccess(0);
p.then(function() {
assert.isTrue(createLockStub.calledOnce, 'Use one lock only.');
assert.isTrue(callback.calledOnce, 'callback called once only.');
}).then(done, done);
});
});
suite('SettingsManagerBase', function() {
var realMozSettings;
suiteSetup(function() {
realMozSettings = navigator.mozSettings;
});
suiteTeardown(function() {
navigator.mozSettings = realMozSettings;
});
test('initSettings, observe change, stopObserve', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var addObserverSpy = this.sinon.spy(mozSettings, 'addObserver');
var removeObserverSpy = this.sinon.spy(mozSettings, 'removeObserver');
var promiseManager = new SettingsPromiseManager();
var settingsManager = new SettingsManagerBase();
settingsManager.promiseManager = promiseManager;
settingsManager.KEYS = ['foo', 'foo2'];
settingsManager.PROPERTIES = ['fooValue', 'foo2Value'];
var p = settingsManager.initSettings();
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledTwice);
assert.isTrue(lockGetSpy.calledWith('foo'));
assert.isTrue(lockGetSpy.calledWith('foo2'));
var req1 = lockGetSpy.getCall(0).returnValue;
req1.fireSuccess({ 'foo': 'bar' });
var req2 = lockGetSpy.getCall(1).returnValue;
req2.fireSuccess({ 'foo2': 'bar2' });
assert.equal(settingsManager.initialized, false);
var p2 = settingsManager.initSettings();
assert.equal(p, p2);
p.then(function(settings) {
assert.equal(settingsManager.initialized, true);
assert.equal(settings.fooValue, 'bar');
assert.equal(settings.foo2Value, 'bar2');
var s = settingsManager.getSettingsSync();
assert.equal(s, settings);
assert.isTrue(addObserverSpy.calledTwice);
assert.isTrue(addObserverSpy.calledWith('foo'));
assert.isTrue(addObserverSpy.calledWith('foo2'));
var calls = 0;
settingsManager.onsettingchange = function(s2) {
calls++;
var s = settingsManager.getSettingsSync();
assert.equal(s2, s);
switch (calls) {
case 1:
assert.equal(s2.fooValue, 'BAR');
break;
case 2:
assert.equal(s2.foo2Value, 'BAR2');
settingsManager.stopObserve();
assert.isTrue(removeObserverSpy.calledTwice);
assert.isTrue(removeObserverSpy.calledWith('foo'));
assert.isTrue(removeObserverSpy.calledWith('foo2'));
done();
break;
default:
assert.isTrue(false, 'called more than twice.');
break;
}
};
mozSettings.dispatchSettingChange('foo', 'BAR');
mozSettings.dispatchSettingChange('foo2', 'BAR2');
}, function(error) {
assert.isTrue(false, 'Should not reject the promise.');
done();
});
});
test('initSettings and returned error', function(done) {
var mozSettings = navigator.mozSettings = new MockNavigatorMozSettings();
var createLockStub = this.sinon.stub(mozSettings, 'createLock');
var lock = new MockNavigatorMozSettingsLock();
var lockGetSpy = this.sinon.spy(lock, 'get');
createLockStub.returns(lock);
var addObserverSpy = this.sinon.spy(mozSettings, 'addObserver');
var removeObserverSpy = this.sinon.spy(mozSettings, 'removeObserver');
var promiseManager = new SettingsPromiseManager();
var settingsManager = new SettingsManagerBase();
settingsManager.promiseManager = promiseManager;
settingsManager.KEYS = ['foo', 'foo2'];
settingsManager.PROPERTIES = ['fooValue', 'foo2Value'];
var p = settingsManager.initSettings();
assert.isTrue(createLockStub.calledOnce);
assert.isTrue(lockGetSpy.calledTwice);
assert.isTrue(lockGetSpy.calledWith('foo'));
assert.isTrue(lockGetSpy.calledWith('foo2'));
var req1 = lockGetSpy.getCall(0).returnValue;
req1.fireError('error');
var req2 = lockGetSpy.getCall(1).returnValue;
req2.fireError('error');
p.then(function() {
assert.isTrue(false, 'Should not resolve the promise.');
done();
}, function(error) {
assert.equal(settingsManager.initialized, false);
assert.equal(addObserverSpy.calledOnce, false);
assert.equal(removeObserverSpy.calledOnce, false);
done();
});
});
});

View file

@ -0,0 +1,121 @@
'use strict';
/* global SoundFeedbackPlayer, MockAudioContext, MockAudioBufferSourceNode,
MockOfflineAudioContext */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/test/unit/mock_audio_context.js');
require('/js/keyboard/sound_feedback_player.js');
suite('SoundFeedbackPlayer', function() {
var audioContextStub;
var offlineAudioContextStub;
var stubAudioBufferSourceNode;
var expectedRequests;
var audioBuffers;
var fakeXhr;
var player;
setup(function(done) {
var requests = [];
expectedRequests = [
{ url: SoundFeedbackPlayer.prototype.CLICK_SOUND_URL,
response: { stub: 'clickerArrayBuffer' } },
{ url: SoundFeedbackPlayer.prototype.SPECIAL_SOUND_URL,
response: { stub: 'specialClickerArrayBuffer' } }
];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
audioBuffers = new Map();
audioBuffers.set(
expectedRequests[0].response, { stub: 'clickerAudioBuffer' });
audioBuffers.set(
expectedRequests[1].response, { stub: 'specialClickerAudioBuffer' });
audioContextStub = new MockAudioContext('system');
this.sinon.stub(window, 'AudioContext').returns(audioContextStub);
offlineAudioContextStub = new MockOfflineAudioContext();
this.sinon.stub(window, 'OfflineAudioContext')
.returns(offlineAudioContextStub);
this.sinon.stub(offlineAudioContextStub, 'decodeAudioData',
function(arrayBuffer, successCallback, errorCallback) {
var audioBuffer = audioBuffers.get(arrayBuffer);
// Simulate async callback
Promise.resolve(audioBuffer).then(successCallback);
});
stubAudioBufferSourceNode =
this.sinon.stub(new MockAudioBufferSourceNode(audioContextStub));
this.sinon.stub(audioContextStub, 'createBufferSource')
.returns(stubAudioBufferSourceNode);
player = new SoundFeedbackPlayer();
var p = player.prepare();
assert.equal(requests.length, 2);
requests.forEach(function(request, i) {
var expectedRequest = expectedRequests[i];
assert.equal(request.url, expectedRequest.url);
assert.equal(request.responseType, 'arraybuffer');
request.response = expectedRequest.response;
request.respond(200, {}, '');
});
p.then(function() { done(); }, done);
});
suite('activate', function() {
setup(function() {
assert.isFalse(window.AudioContext.called);
player.activate();
assert.isTrue(window.AudioContext.calledOnce);
});
test('mozAudioChannelType is system', function() {
assert.isTrue(window.AudioContext.calledWith('system'));
assert.equal(player._audioCtx.mozAudioChannelType, 'system');
});
test('activate again', function() {
player.activate();
assert.isFalse(window.AudioContext.calledTwice, 'caused a side effect');
});
test('deactivate', function() {
player.deactivate();
// XXX probing "private" variable since there is no way to tell
// if the reference has been released without doing so.
assert.equal(player._audioCtx, null);
});
test('play normal clicker', function() {
player.play(false);
var audioBuffer = audioBuffers.get(expectedRequests[0].response);
assert.equal(stubAudioBufferSourceNode.buffer, audioBuffer);
assert.isTrue(stubAudioBufferSourceNode.connect
.calledWith(audioContextStub.destination));
assert.isTrue(stubAudioBufferSourceNode.start.calledWith(0));
});
test('play special clicker', function() {
player.play(true);
var audioBuffer = audioBuffers.get(expectedRequests[1].response);
assert.equal(stubAudioBufferSourceNode.buffer, audioBuffer);
assert.isTrue(stubAudioBufferSourceNode.connect
.calledWith(audioContextStub.destination));
assert.isTrue(stubAudioBufferSourceNode.start.calledWith(0));
});
});
});

View file

@ -0,0 +1,369 @@
'use strict';
/* global LayoutManager, InputMethodManager, L10nLoader, CandidatePanelManager,
ActiveTargetsManager, UpperCaseStateManager, SettingsPromiseManager,
LayoutRenderingManager, KeyboardConsole, LayoutLoader,
InputMethodLoader, FeedbackManager, StateManager,
AbortablePromiseQueue,
MockInputMethod, MockInputContext, MockEventTarget */
require('/js/keyboard/console.js');
require('/js/keyboard/input_method_manager.js');
require('/js/keyboard/layout_manager.js');
require('/js/keyboard/layout_loader.js');
require('/js/keyboard/settings.js');
require('/js/keyboard/l10n_loader.js');
require('/js/keyboard/active_targets_manager.js');
require('/js/keyboard/candidate_panel_manager.js');
require('/js/keyboard/feedback_manager.js');
require('/js/keyboard/upper_case_state_manager.js');
require('/js/keyboard/layout_rendering_manager.js');
require('/js/keyboard/abortable_promise_queue.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_navigator_input_method.js');
require('/js/keyboard/state_manager.js');
suite('StateManager', function() {
var manager;
var app;
var realMozInputMethod;
var windowStub;
var stubAbortablePromiseQueue;
suiteSetup(function() {
Object.defineProperty(
document, 'hidden',
{
value: true,
writable: true,
configurable: true
});
});
setup(function() {
stubAbortablePromiseQueue =
this.sinon.stub(AbortablePromiseQueue.prototype);
this.sinon.stub(window, 'AbortablePromiseQueue')
.returns(stubAbortablePromiseQueue);
app = {
layoutManager: this.sinon.stub(LayoutManager.prototype),
inputMethodManager: this.sinon.stub(InputMethodManager.prototype),
l10nLoader: this.sinon.stub(L10nLoader.prototype),
candidatePanelManager: this.sinon.stub(CandidatePanelManager.prototype),
feedbackManager: this.sinon.stub(FeedbackManager.prototype),
targetHandlersManager: {
activeTargetsManager: this.sinon.stub(ActiveTargetsManager.prototype)
},
upperCaseStateManager: this.sinon.stub(UpperCaseStateManager.prototype),
setInputContext: this.sinon.stub(),
settingsPromiseManager: this.sinon.stub(SettingsPromiseManager.prototype),
layoutRenderingManager: this.sinon.stub(LayoutRenderingManager.prototype),
console: this.sinon.stub(KeyboardConsole.prototype)
};
app.layoutManager.loader = this.sinon.stub(LayoutLoader.prototype);
app.inputMethodManager.loader =
this.sinon.stub(InputMethodLoader.prototype);
windowStub = new MockEventTarget();
this.sinon.stub(window, 'addEventListener',
windowStub.addEventListener.bind(windowStub));
this.sinon.stub(window, 'removeEventListener',
windowStub.removeEventListener.bind(windowStub));
window.location.hash = '#foo';
realMozInputMethod = navigator.mozInputMethod;
navigator.mozInputMethod = new MockInputMethod();
this.sinon.stub(window, 'setTimeout');
manager = new StateManager(app);
});
teardown(function() {
manager.stop();
assert.isTrue(
window.removeEventListener.calledWith('hashchange', manager));
assert.isTrue(
window.removeEventListener.calledWith('visibilitychange', manager));
navigator.mozInputMethod = realMozInputMethod;
});
suite('start with not hidden', function() {
setup(function() {
document.hidden = false;
navigator.mozInputMethod.setInputContext(new MockInputContext());
app.inputContext = navigator.mozInputMethod.inputcontext;
app.layoutManager.currentPage = {
imEngine: 'bar'
};
manager.start();
assert.isTrue(
window.addEventListener.calledWith('hashchange', manager));
assert.isTrue(
window.addEventListener.calledWith('visibilitychange', manager));
assert.isFalse(app.layoutManager.loader.getLayoutAsync.calledOnce,
'Should not try to preload layout.');
assert.isFalse(
app.inputMethodManager.loader.getInputMethodAsync.calledOnce,
'Should not try to preload IMEngine.');
assert.isFalse(app.l10nLoader.load.calledOnce);
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.start.calledOnce);
assert.isTrue(stubAbortablePromiseQueue.run.calledOnce);
var value;
stubAbortablePromiseQueue.run.firstCall.args[0].forEach(function(task) {
value = task(value);
});
assert.isTrue(app.inputMethodManager.deactivateIMEngine.calledOnce);
assert.isTrue(app.candidatePanelManager.hideFullPanel.calledOnce);
assert.isTrue(app.candidatePanelManager.updateCandidates.calledOnce);
assert.isFalse(app.targetHandlersManager
.activeTargetsManager.clearAllTargets.called);
assert.isTrue(app.feedbackManager.activate.calledOnce);
assert.isTrue(app.layoutManager.switchCurrentLayout.calledWith('foo'));
assert.isTrue(app.upperCaseStateManager.reset.calledOnce);
assert.isTrue(app.candidatePanelManager.reset.calledOnce);
assert.isTrue(
app.inputMethodManager.activateIMEngine.calledWith('bar'));
assert.isTrue(app.upperCaseStateManager.reset.calledBefore(
app.inputMethodManager.activateIMEngine),
'Reset the state before engine activates.');
assert.isTrue(
app.layoutRenderingManager.updateLayoutRendering.calledOnce);
assert.isTrue(app.l10nLoader.load.calledOnce);
assert.isTrue(app.l10nLoader.load.calledOn(app.l10nLoader));
});
test('hashchange', function() {
window.location.hash = '#foo2';
var evt = {
type: 'hashchange'
};
app.layoutManager.currentPage = {
imEngine: 'bar2'
};
windowStub.dispatchEvent(evt);
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.run.calledTwice);
var value;
stubAbortablePromiseQueue.run.getCall(1).args[0].forEach(function(task) {
value = task(value);
});
// Start layout switching
assert.isTrue(app.candidatePanelManager.hideFullPanel.calledTwice);
assert.isTrue(app.candidatePanelManager.updateCandidates.calledTwice);
assert.isFalse(app.targetHandlersManager
.activeTargetsManager.clearAllTargets.called);
assert.isTrue(app.layoutManager.switchCurrentLayout.calledWith('foo2'));
assert.isTrue(app.upperCaseStateManager.reset.calledTwice);
assert.isTrue(
app.inputMethodManager.activateIMEngine.calledWith('bar2'));
assert.isTrue(app.upperCaseStateManager.reset.calledBefore(
app.inputMethodManager.activateIMEngine),
'Reset the state before engine activates.');
assert.isTrue(
app.layoutRenderingManager.updateLayoutRendering.calledTwice);
assert.isTrue(app.l10nLoader.load.calledTwice);
assert.isTrue(app.l10nLoader.load.calledOn(app.l10nLoader));
});
suite('deactivate', function() {
setup(function() {
// triggers inputcontextchange
navigator.mozInputMethod.setInputContext();
app.inputContext = null;
});
test('finishing deactivation', function() {
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.run.calledTwice);
var value;
stubAbortablePromiseQueue.run.getCall(1).args[0]
.forEach(function(task) {
value = task(value);
});
assert.isTrue(app.candidatePanelManager.hideFullPanel.calledTwice);
assert.isTrue(app.candidatePanelManager.updateCandidates.calledTwice);
assert.isTrue(app.targetHandlersManager
.activeTargetsManager.clearAllTargets.calledOnce);
assert.isTrue(app.feedbackManager.deactivate.calledOnce);
assert.isTrue(app.inputMethodManager.deactivateIMEngine.calledTwice);
// trigger visibilitychange (after inputcontextchange)
// check if deactivation should not be called again by
// checking hideFullPanel as an example
app.candidatePanelManager.hideFullPanel.reset();
document.hidden = true;
var evt = {
type: 'visibilitychange'
};
windowStub.dispatchEvent(evt);
assert.isFalse(app.candidatePanelManager.hideFullPanel.called,
'should not attempt to hide again.');
});
});
});
suite('start with hidden', function() {
setup(function() {
document.hidden = true;
var layout = { imEngine: 'bar' };
var getLayoutAsyncPromise = {};
app.layoutManager.loader.getLayoutAsync.returns(getLayoutAsyncPromise);
manager.start();
assert.isTrue(
window.addEventListener.calledWith('hashchange', manager));
assert.isTrue(
window.addEventListener.calledWith('visibilitychange', manager));
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.start.calledOnce);
assert.isTrue(stubAbortablePromiseQueue.run.calledOnce);
var value;
stubAbortablePromiseQueue.run.firstCall.args[0].forEach(function(task) {
// "resolve" this promise
if (value === getLayoutAsyncPromise) {
value = layout;
}
value = task(value);
});
assert.isTrue(app.layoutManager.loader.getLayoutAsync.calledWith('foo'));
assert.isTrue(
app.inputMethodManager.loader.getInputMethodAsync.calledWith('bar'));
assert.isTrue(app.l10nLoader.load.calledOnce);
assert.isTrue(app.l10nLoader.load.calledOn(app.l10nLoader));
});
test('hashchange', function() {
window.location.hash = '#foo2';
var evt = {
type: 'hashchange'
};
var layout = { imEngine: 'bar2' };
var getLayoutAsyncPromise = {};
app.layoutManager.loader.getLayoutAsync.returns(getLayoutAsyncPromise);
windowStub.dispatchEvent(evt);
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.run.calledTwice);
var value;
stubAbortablePromiseQueue.run.getCall(1).args[0].forEach(function(task) {
// "resolve" this promise
if (value === getLayoutAsyncPromise) {
value = layout;
}
value = task(value);
});
assert.isTrue(app.layoutManager.loader.getLayoutAsync.calledWith('foo2'),
'Start loading foo2 when hashchanged');
assert.isTrue(
app.inputMethodManager.loader.getInputMethodAsync.calledWith('bar2'));
});
suite('activate', function() {
setup(function() {
document.hidden = false;
var evt = {
type: 'visibilitychange'
};
windowStub.dispatchEvent(evt);
assert.isFalse(app.layoutManager.switchCurrentLayout.calledOnce,
'Not launched yet with visibilitychange only.');
app.layoutManager.currentPage = {
imEngine: 'bar'
};
navigator.mozInputMethod.setInputContext(new MockInputContext());
app.inputContext = navigator.mozInputMethod.inputcontext;
assert.isFalse(app.layoutManager.loader.getLayoutAsync.calledTwice,
'Should not try to preload layout again.');
assert.isFalse(
app.inputMethodManager.loader.getInputMethodAsync.calledTwice,
'Should not try to preload IMEngine again.');
assert.isFalse(app.l10nLoader.load.calledTwice);
});
test('finishing activation', function() {
// Run the queue
assert.isTrue(stubAbortablePromiseQueue.run.calledTwice);
var value;
stubAbortablePromiseQueue.run.getCall(1).args[0]
.forEach(function(task) {
value = task(value);
});
assert.isTrue(app.inputMethodManager.deactivateIMEngine.calledOnce);
assert.isTrue(app.candidatePanelManager.hideFullPanel.calledOnce);
assert.isTrue(app.candidatePanelManager.updateCandidates.calledOnce);
assert.isFalse(app.targetHandlersManager
.activeTargetsManager.clearAllTargets.called);
assert.isTrue(app.feedbackManager.activate.calledOnce);
assert.isTrue(app.layoutManager.switchCurrentLayout.calledWith('foo'));
assert.isTrue(app.upperCaseStateManager.reset.calledOnce);
assert.isTrue(
app.inputMethodManager.activateIMEngine.calledWith('bar'));
assert.isTrue(app.upperCaseStateManager.reset.calledBefore(
app.inputMethodManager.activateIMEngine),
'Reset the state before engine activates.');
assert.isTrue(
app.layoutRenderingManager.updateLayoutRendering.calledOnce);
assert.isTrue(app.l10nLoader.load.calledTwice);
assert.isTrue(app.l10nLoader.load.getCall(1).calledOn(app.l10nLoader));
});
});
});
});

View file

@ -0,0 +1,404 @@
'use strict';
/* global TargetHandlersManager, ActiveTargetsManager,
DefaultTargetHandler, KeyEvent, LayoutManager, KeyboardConsole */
require('/js/keyboard/active_targets_manager.js');
require('/js/keyboard/target_handlers.js');
require('/js/keyboard/layout_manager.js');
require('/js/keyboard/console.js');
require('/js/keyboard/target_handlers_manager.js');
suite('TargetHandlersManager', function() {
var manager;
var app;
var handlerStub;
var activeTargetsManagerStub;
var actions = [
{name: 'commit', eventHandler: 'ontargetcommitted' },
{name: 'move', eventHandler: 'ontargetmoved' },
{name: 'moveOut', eventHandler: 'ontargetmovedout' },
{name: 'cancel', eventHandler: 'ontargetcancelled' },
{name: 'doubleTap', eventHandler: 'ontargetdoubletapped' },
{name: 'newTargetActivate', eventHandler: 'onnewtargetwillactivate' }
];
setup(function() {
activeTargetsManagerStub = this.sinon.stub(ActiveTargetsManager.prototype);
this.sinon.stub(window, 'ActiveTargetsManager')
.returns(activeTargetsManagerStub);
handlerStub = this.sinon.stub(DefaultTargetHandler.prototype);
this.sinon.stub(window, 'DefaultTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'NullTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'SpaceKeyTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'CandidateSelectionTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'BackspaceTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'CompositeTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'PageSwitchingTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'CapsLockTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'SwitchKeyboardTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'ToggleCandidatePanelTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'DismissSuggestionsTargetHandler')
.returns(handlerStub);
this.sinon.stub(window, 'HandwritingPadTargetHandler')
.returns(handlerStub);
app = {
console: this.sinon.stub(KeyboardConsole.prototype),
layoutManager: this.sinon.stub(LayoutManager.prototype)
};
manager = new TargetHandlersManager(app);
manager.start();
assert.isTrue(window.ActiveTargetsManager.calledWith(app));
assert.isTrue(activeTargetsManagerStub.start.calledOnce);
});
suite('activate', function() {
var target;
setup(function(done) {
target = {
keyCode: 99
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(window.DefaultTargetHandler.calledWith(target, app));
assert.isTrue(window.DefaultTargetHandler.calledOnce);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
teardown(function() {
assert.isTrue(window.DefaultTargetHandler.calledOnce,
'Same handler was used for every following action.');
});
actions.forEach(function(action) {
test(action.name, function(done) {
activeTargetsManagerStub[action.eventHandler](target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub[action.name].calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
suite('longPress', function() {
setup(function(done) {
activeTargetsManagerStub.ontargetlongpressed(target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.longPress.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
actions.forEach(function(action) {
test(action.name, function(done) {
activeTargetsManagerStub[action.eventHandler](target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub[action.name].calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
});
});
suite('moveIn', function() {
var target;
setup(function(done) {
target = {
keyCode: 99
};
activeTargetsManagerStub.ontargetmovedin(target);
assert.isTrue(window.DefaultTargetHandler.calledWith(target, app));
assert.isTrue(window.DefaultTargetHandler.calledOnce);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.moveIn.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
teardown(function() {
assert.isTrue(window.DefaultTargetHandler.calledOnce,
'Same handler was used for every following action.');
});
actions.forEach(function(action) {
test(action.name, function(done) {
activeTargetsManagerStub[action.eventHandler](target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub[action.name].calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
suite('longPress', function() {
setup(function(done) {
activeTargetsManagerStub.ontargetlongpressed(target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.longPress.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
actions.forEach(function(action) {
test(action.name, function(done) {
activeTargetsManagerStub[action.eventHandler](target);
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub[action.name].calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
});
});
suite('activate different targets', function() {
test('DismissSuggestionsTargetHandler', function(done) {
var target = {
isDismissSuggestionsButton: true
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.DismissSuggestionsTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('CandidateSelectionTargetHandler', function(done) {
var target = {
suggestion: 'foo'
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.CandidateSelectionTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('CompositeTargetHandler', function(done) {
var target = {
compositeKey: 'lol'
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.CompositeTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('BackspaceTargetHandler', function(done) {
var target = {
keyCode: KeyEvent.DOM_VK_BACK_SPACE
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.BackspaceTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('SpaceKeyTargetHandler', function(done) {
var target = {
keyCode: KeyEvent.DOM_VK_SPACE
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.SpaceKeyTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('PageSwitchingTargetHandler', function(done) {
var target = {
keyCode: KeyEvent.DOM_VK_ALT
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.PageSwitchingTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('SwitchKeyboardTargetHandler', function(done) {
var target = {
keyCode: app.layoutManager.KEYCODE_SWITCH_KEYBOARD
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.SwitchKeyboardTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('CapsLockTargetHandler', function(done) {
var target = {
keyCode: KeyEvent.DOM_VK_CAPS_LOCK
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.CapsLockTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
suite('DefaultTargetHandler', function() {
test('-99', function(done) {
var target = {
keyCode: -99
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.DefaultTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('99', function(done) {
var target = {
keyCode: 99
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.DefaultTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
test('NullTargetHandler', function(done) {
var target = {};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.NullTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
test('HandwritingPadTargetHandler', function(done) {
var target = {
isHandwritingPad: true
};
activeTargetsManagerStub.ontargetactivated(target);
assert.isTrue(
window.HandwritingPadTargetHandler.calledWith(target, app));
manager.promiseQueue.then(function() {
assert.isTrue(handlerStub.activate.calledOnce);
assert.isTrue(activeTargetsManagerStub.blockNewUserPress);
assert.isTrue(activeTargetsManagerStub.blockTargetMovedOut);
activeTargetsManagerStub.ontargetcommitted(target);
}).then(function() {
assert.isTrue(handlerStub.commit.calledOnce);
assert.isFalse(activeTargetsManagerStub.blockNewUserPress);
assert.isFalse(activeTargetsManagerStub.blockTargetMovedOut);
activeTargetsManagerStub.ontargetcommitted(target);
}, function(e) {
assert.isTrue(false, 'promiseQueue should not reject.');
}).then(done, done);
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,282 @@
'use strict';
/* global UpperCaseStateManager */
require('/js/keyboard/upper_case_state_manager.js');
suite('UpperCaseStateManager', function() {
var manager;
setup(function() {
manager = new UpperCaseStateManager();
manager.onstatechange = this.sinon.stub();
manager.start();
});
suite('isUpperCase = false, isUpperCaseLocked = false', function() {
test('set isUpperCase = true', function() {
manager.switchUpperCaseState({
isUpperCase: true
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false', function() {
manager.switchUpperCaseState({
isUpperCase: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: false
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
});
suite('isUpperCase = true, isUpperCaseLocked = false', function() {
setup(function() {
manager.isUpperCase = true;
});
test('set isUpperCase = true', function() {
manager.switchUpperCaseState({
isUpperCase: true
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false', function() {
manager.switchUpperCaseState({
isUpperCase: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: false
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: false
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
});
suite('isUpperCase = true, isUpperCaseLocked = true', function() {
setup(function() {
manager.isUpperCase = true;
manager.isUpperCaseLocked = true;
});
test('set isUpperCase = true', function() {
manager.switchUpperCaseState({
isUpperCase: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false', function() {
manager.switchUpperCaseState({
isUpperCase: false
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: false
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: false
});
assert.isFalse(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
test('set isUpperCase = true, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: true,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCase = false, isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCase: false,
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = true', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: true
});
assert.isTrue(manager.isUpperCase);
assert.isTrue(manager.isUpperCaseLocked);
assert.isFalse(manager.onstatechange.calledOnce);
});
test('set isUpperCaseLocked = false', function() {
manager.switchUpperCaseState({
isUpperCaseLocked: false
});
assert.isTrue(manager.isUpperCase);
assert.isFalse(manager.isUpperCaseLocked);
assert.isTrue(manager.onstatechange.calledOnce);
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,400 @@
'use strict';
/* global ViewManager, KeyboardConsole*/
require('/js/keyboard/view_manager.js');
require('/js/keyboard/console.js');
require('/js/views/key_view.js');
require('/js/views/base_view.js');
require('/js/views/layout_page_view.js');
require('/js/views/candidate_panel_view.js');
require('/js/views/latin_candidate_panel_view.js');
require('/js/views/view_utils.js');
suite('View Manager', function() {
suiteSetup(function() {
window.perfTimer = {
printTime: function() {},
startTimer: function() {}
};
});
suiteTeardown(function() {
window.perfTimer = null;
});
var app = null;
var viewManager = null;
var fakeRenderingManager;
var stubRequestAnimationFrame;
var container, activeIme;
setup(function(next) {
fakeRenderingManager = {
getTargetObject: function(elem) {
return this.domObjectMap.get(elem);
},
domObjectMap: new WeakMap()
};
// Tests in CI do not necessarily run at the same resolution as a device.
Object.defineProperty(window, 'innerWidth', {
configurable: true,
get: () => 320 }
);
Object.defineProperty(window, 'innerHeight', {
configurable: true,
get: () => 480 }
);
stubRequestAnimationFrame =
this.sinon.stub(window, 'requestAnimationFrame');
document.body.innerHTML = '';
container = document.createElement('div');
container.id = 'keyboard';
document.body.appendChild(container);
activeIme = document.createElement('div');
container.appendChild(activeIme);
Object.defineProperty(container, 'clientWidth', makeDescriptor(320));
app = {
layoutRenderingManager: fakeRenderingManager,
console: this.sinon.stub(KeyboardConsole.prototype)
};
viewManager = new ViewManager(app);
viewManager.start();
loadKeyboardStyle(next);
});
function makeDescriptor(val) {
return {
configurable: true,
enumerable: true,
value: val,
writable: true
};
}
function loadKeyboardStyle(callback) {
// Dirty trick http://www.phpied.com/when-is-a-stylesheet-really-loaded/
var style = document.createElement('style');
style.textContent = '@import "../../style/keyboard.css"';
var fi = setInterval(function() {
try {
style.sheet.cssRules;
clearInterval(fi);
callback();
} catch (e) {}
}, 10);
document.body.appendChild(style);
}
test('screenInPortraitMode', function() {
assert.isTrue(viewManager.screenInPortraitMode());
});
test('getRemToPx', function() {
assert.equal(viewManager.getRemToPx(), 320 / 32);
});
suite(' > resize()', function() {
test('Add portrait class to IME in portrait mode', function() {
viewManager.cachedWindowWidth = 200;
viewManager.cachedWindowHeight = 400;
viewManager.resize();
assert.equal(viewManager.container.classList.contains('portrait'), true);
assert.equal(viewManager.container.classList.contains('landscape'),
false);
});
test('Add landscape class to IME in landscape mode', function() {
viewManager.cachedWindowWidth = 400;
viewManager.cachedWindowHeight = 200;
viewManager.resize();
assert.equal(viewManager.container.classList.contains('landscape'), true);
assert.equal(viewManager.container.classList.contains('portrait'), false);
});
test('All keys should have same visual width if fully used', function() {
var layout = {
width: 10,
keys: [
[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
]
};
viewManager.render(layout);
viewManager.resize();
var all = [].map.call(document.querySelectorAll('.visual-wrapper'),
function(el) {
return el.clientWidth;
});
assert.notEqual(all[0], 0);
assert.equal(all.every(function(el) {
return el === all[0];
}), true, 'Every element has width ' + all[0]);
});
test('Key with ratio 2 should be twice as big', function(next) {
var layout = {
width: 3,
keys: [
[{ ratio: 2 }, {}]
]
};
viewManager.render(layout, null, function() {
var all = document.querySelectorAll('.keyboard-key');
assert.equal(all[0].style.flexGrow, 2);
next();
});
stubRequestAnimationFrame.getCall(0).args[0]();
});
test('Side keys should fill up space', function(next) {
var layout = {
width: 4,
keys: [
[{}, {}, {}]
]
};
viewManager.render(layout, null, function() {
var visual = document.querySelectorAll('.visual-wrapper');
assert.equal(visual[0].clientWidth, visual[1].clientWidth,
'Visually same');
var keys = document.querySelectorAll('.keyboard-key');
assert.equal(keys[0].style.flexGrow, 1.5);
next();
});
stubRequestAnimationFrame.getCall(0).args[0]();
});
test('Sidekeys should adjust space when rotating', function(next) {
viewManager.cachedWindowWidth = 400;
viewManager.cachedWindowHeight = 200;
Object.defineProperty(container, 'clientWidth',
makeDescriptor(400));
var layout = {
width: 4,
keys: [
[{}, {}, {}]
]
};
viewManager.render(layout, null, function() {
var visual = document.querySelectorAll('.visual-wrapper');
viewManager.cachedWindowWidth = 700;
viewManager.cachedWindowHeight = 1000;
Object.defineProperty(container, 'clientWidth', makeDescriptor(700));
viewManager.resize(function() {
assert.equal(visual[0].clientWidth, visual[1].clientWidth,
'Visually same');
next();
});
stubRequestAnimationFrame.getCall(1).args[0]();
});
stubRequestAnimationFrame.getCall(0).args[0]();
});
test('getKeyArray()', function() {
viewManager.currentPageView = {
getVisualData: sinon.stub()
};
viewManager.getKeyArray();
assert.ok(viewManager.currentPageView.getVisualData.called);
});
});
suite('> render()', function() {
test('candidate-panel class shouldnt be set if flag isnt set', function() {
var layout = {
width: 1,
keys: []
};
viewManager.render(layout);
assert.equal(viewManager.container.classList.contains('candidate-panel'),
false);
});
suite('UpdateByIMEngine', function() {
test('page shouldnt be cached if updateByIMEngine is true', function() {
var layout = {
keys: [],
updateByIMEngine: true
};
viewManager.render(layout);
this.sinon.stub(viewManager.pageViews, 'set');
assert.equal(viewManager.pageViews.set.called, false);
});
test('updateByIMEngine page should be destoryed when switch', function() {
var layout = {
keys: [],
layoutName: 'cachedLayout',
};
var layout2 = {
keys: [],
updateByIMEngine: true
};
var firstPage, secondPage;
viewManager.render(layout);
firstPage = viewManager.currentPageView;
this.sinon.stub(firstPage, 'destroy');
viewManager.render(layout2);
secondPage = viewManager.currentPageView;
this.sinon.stub(secondPage, 'destroy');
viewManager.render(layout);
assert.equal(firstPage.destroy.called, false);
assert.equal(secondPage.destroy.calledOnce, true);
});
});
suite('Dimensions', function() {
function createDimensionTest(rowCount, orientation, suggest, latin,
next) {
var layout = {
width: (Math.random() * 8 | 0) + 2,
keys: [],
layoutName: 'test',
imEngine: latin ? 'latin' : 'nonLatin'
};
for (var ri = 0; ri < rowCount; ri++) {
var r = [];
for (var ki = 0, kl = (Math.random() * 8 | 0) + 2; ki < kl; ki++) {
r.push({ value: (Math.random() * 26 | 0) + 97 });
}
layout.keys.push(r);
}
if (orientation === 'landscape') {
document.querySelector('#keyboard').classList.remove('portrait');
document.querySelector('#keyboard').classList.add('landscape');
}
else {
document.querySelector('#keyboard').classList.add('portrait');
document.querySelector('#keyboard').classList.remove('landscape');
}
var flags = suggest ?
{ showCandidatePanel: true } :
{};
viewManager.render(layout, flags, function() {
var imeHeight = viewManager.currentPageView.element.scrollHeight;
assert.equal(imeHeight, viewManager.getHeight());
next();
});
stubRequestAnimationFrame.getCall(0).args[0]();
}
var rows = [0, 1, 2, 3, 4, 5];
var orientation = ['portrait', 'landscape'];
var suggest = [true, false];
var latin = [true, false];
rows.forEach(function(row) {
orientation.forEach(function(orientation) {
suggest.forEach(function(suggest) {
latin.forEach(function(latin) {
var name = [
'getHeight',
row + ' rows',
orientation,
(suggest ? '' : 'no') + ' suggest',
latin ? 'latin' : ''
].join(', ');
test(name, function(next) {
createDimensionTest(row, orientation, suggest, latin, next);
});
});
});
});
});
});
});
suite('Highlight Keys', function() {
var dummyKey = {
dummy: 'dummy'
};
setup(function(){
var layout = {
width: 2,
keys: [
[{}, {}]
]
};
viewManager.render(layout);
});
test('Highlight a key with uppercase', function() {
var keyView = {
highlight: this.sinon.stub(),
element: {}
};
viewManager.registerView(dummyKey, keyView);
viewManager.setUpperCaseLock({
isUpperCase: true,
isUpperCaseLocked: false
});
viewManager.highlightKey(dummyKey);
assert.isTrue(keyView.highlight.called);
assert.isTrue(keyView.highlight.calledWith({upperCase: true}));
});
test('Highlight a key with lowercase', function() {
var keyView = {
highlight: this.sinon.stub(),
element: {}
};
viewManager.registerView(dummyKey, keyView);
viewManager.setUpperCaseLock({
isUpperCase: false,
isUpperCaseLocked: false
});
viewManager.highlightKey(dummyKey);
assert.isTrue(keyView.highlight.called);
assert.isTrue(keyView.highlight.calledWith({upperCase: false}));
});
});
});

View file

@ -0,0 +1,88 @@
'use strict';
/* global VisualHighlightManager */
require('/js/keyboard/visual_highlight_manager.js');
suite('VisualHighlightManager', function() {
var app;
var manager;
var target;
var viewManager;
setup(function() {
// Create fake viewManager
viewManager = {
highlightKey: this.sinon.stub(),
unHighlightKey: this.sinon.stub()
};
app = {
viewManager: viewManager
};
target = {};
manager = new VisualHighlightManager(app);
manager.start();
this.sinon.stub(window, 'setTimeout');
this.sinon.stub(window, 'clearTimeout');
});
teardown(function() {
app = null;
});
test('show', function() {
manager.show(target);
assert.isTrue(viewManager.highlightKey.calledWith(target));
});
test('show after hide', function() {
manager.hide(target);
var target2 = {};
manager.show(target2);
assert.isTrue(viewManager.unHighlightKey.calledWith(target),
'The first target highlight should be hidden immediately.');
assert.isTrue(viewManager.highlightKey.calledWith(target2));
});
test('hide', function() {
manager.hide(target);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(window.setTimeout.getCall(0).args[1],
manager.HIGHTLIGHT_DELAY_MS);
window.setTimeout.getCall(0).args[0].call(window);
assert.isTrue(viewManager.unHighlightKey.calledWith(target));
});
test('hide twice within HIGHTLIGHT_DELAY_MS', function() {
window.setTimeout.returns(200);
manager.hide(target);
assert.isTrue(window.setTimeout.calledOnce);
assert.equal(window.setTimeout.getCall(0).args[1],
manager.HIGHTLIGHT_DELAY_MS);
window.setTimeout.returns(201);
manager.hide(target);
assert.isTrue(window.clearTimeout.calledWith(200));
assert.isTrue(window.setTimeout.calledTwice);
window.setTimeout.getCall(1).args[0].call(window);
assert.isTrue(viewManager.unHighlightKey.calledWith(target));
});
});

View file

@ -0,0 +1,113 @@
'use strict';
/* global MockEventTarget */
/**
* This file provide incomplete interfaces of Web Audio API for users to
* create stub on top of.
* New interfaces can simply be added by manually copying and editing WebIDL
* from Gecko source tree.
*/
var MockAudioNode = function(ctx) {
this.context = ctx;
};
MockAudioNode.prototype = Object.create(MockEventTarget.prototype);
MockAudioNode.prototype.context = null;
MockAudioNode.prototype.numberOfInputs = 0;
MockAudioNode.prototype.numberOfOutputs = 0;
MockAudioNode.prototype.channelCount = 2;
MockAudioNode.prototype.channelCountMode = 'explicit';
MockAudioNode.prototype.channelInterpretation = 'speakers';
MockAudioNode.prototype.connect =
MockAudioNode.prototype.disconnect = function() {
throw 'MockAudioNode: Unimplemented. Stub me?';
};
var MockAudioDestinationNode = function(ctx) {
MockAudioNode.call(this, ctx);
this.numberOfInputs = 1;
this.numberOfOutputs = 0;
};
MockAudioDestinationNode.prototype = Object.create(MockAudioNode.prototype);
MockAudioDestinationNode.prototype.maxChannelCount = 2;
var MockAudioBufferSourceNode = function(ctx) {
MockAudioNode.call(this, ctx);
this.numberOfInputs = 0;
this.numberOfOutputs = 1;
// Unimplemented
// this.playbackRate = new MockAudioParam();
};
MockAudioBufferSourceNode.prototype = Object.create(MockAudioNode.prototype);
MockAudioBufferSourceNode.prototype.playbackRate = null;
MockAudioBufferSourceNode.prototype.buffer = null;
MockAudioBufferSourceNode.prototype.loop = false;
MockAudioBufferSourceNode.prototype.loopEnd = 0;
MockAudioBufferSourceNode.prototype.loopStart = 0;
MockAudioBufferSourceNode.prototype.onended = null;
MockAudioBufferSourceNode.prototype.start =
MockAudioBufferSourceNode.prototype.stop = function() {
throw 'MockAudioBufferSourceNode: Unimplemented. Stub me?';
};
var MockAudioListener = function() {
};
MockAudioListener.prototype.dopplerFactor = 1;
MockAudioListener.prototype.speedOfSound = 343.3;
MockAudioListener.prototype.setPosition =
MockAudioListener.prototype.setOrientation =
MockAudioListener.prototype.setVelocity = function() {
throw 'MockAudioListener: Unimplemented. Stub me?';
};
var MockAudioContext = function(type) {
this.destination = new MockAudioDestinationNode(this);
this.listener = new MockAudioListener();
if (type) {
this.mozAudioChannelType = type;
}
};
MockAudioContext.prototype = Object.create(MockEventTarget.prototype);
MockAudioContext.prototype.destination = null;
MockAudioContext.prototype.sampleRate = 48000;
MockAudioContext.prototype.currentTime = 0;
MockAudioContext.prototype.listener = null;
MockAudioContext.prototype.mozAudioChannelType = 'normal';
MockAudioContext.prototype.onmozinterruptbegin = null;
MockAudioContext.prototype.onmozinterruptend = null;
MockAudioContext.prototype.decodeAudioData =
MockAudioContext.prototype.createBufferSource =
MockAudioContext.prototype.createMediaStreamDestination =
MockAudioContext.prototype.createScriptProcessor =
MockAudioContext.prototype.createAnalyser =
MockAudioContext.prototype.createMediaElementSource =
MockAudioContext.prototype.createMediaStreamSource =
MockAudioContext.prototype.createGain =
MockAudioContext.prototype.createDelay =
MockAudioContext.prototype.createBiquadFilter =
MockAudioContext.prototype.createWaveShaper =
MockAudioContext.prototype.createPanner =
MockAudioContext.prototype.createConvolver =
MockAudioContext.prototype.createChannelSplitter =
MockAudioContext.prototype.createChannelMerger =
MockAudioContext.prototype.createDynamicsCompressor =
MockAudioContext.prototype.createOscillator =
MockAudioContext.prototype.createPeriodicWave = function() {
throw 'MockAudioContext: Unimplemented. Stub me?';
};
var MockOfflineAudioContext = function(numOfChannels, length, sampleRate) {
MockAudioContext.call(this);
};
MockOfflineAudioContext.prototype = Object.create(MockAudioContext.prototype);
MockOfflineAudioContext.prototype.oncomplete = null;
MockOfflineAudioContext.prototype.startRendering = function() {
throw 'MockOfflineAudioContext: Unimplemented. Stub me?';
};

View file

@ -0,0 +1,246 @@
'use strict';
/* global MockEventTarget */
/**
* This file implement a set of nearly empty functions for you to
* spy/stub on top of it. The native constructors are in fact exposed
* (try console.log(IDBFactory) for example), but stub these native
* objects or object prototypes might be to troublesome for writing
* tests, hence the mock.
*
* This file requires MockEventTarget.
*
* These constructors contains methods from the formal spec:
* http://www.w3.org/TR/IndexedDB/
*
* You may also find Gecko implementation here:
* http://dxr.mozilla.org/mozilla-central/source/dom/indexedDB/
* and it's WebIDL files in:
* http://dxr.mozilla.org/mozilla-central/source/dom/webidl/
*
* Since window.indexedDB is not overwritable in Gecko,
* we would have to overwrite the methods instead.
*
* To use the mock, do this in your setup():
*
* var stubIndexedDB = this.sinon.stub(new MockIDBFactory());
* MockIDBFactory.attachToWindow(stubIndexedDB);
*
* In the test(), do this after the code to test to, for example, respond to
* database open request:
*
* var req = stubIndexedDB.open.firstCall.returnValue;
* var stubIDB = req.result = this.sinon.stub(new MockIDBDatabase(dbOptions));
* req.dispatchEvent({
* type: 'success',
* target: req
* });
*
*/
//(function(exports) {
var MockIDBRequest = function(mOptions) {
this.transaction = mOptions.transaction;
this.source = mOptions.store;
this.readyState = 'pending';
};
MockIDBRequest.prototype = Object.create(MockEventTarget.prototype);
MockIDBRequest.prototype.result = null;
MockIDBRequest.prototype.error = null;
MockIDBRequest.prototype.source = null;
MockIDBRequest.prototype.transaction = null;
MockIDBRequest.prototype.readyState = null;
MockIDBRequest.prototype.onsuccess = null;
MockIDBRequest.prototype.onerror = null;
var MockIDBOpenDBRequest = function() {
};
MockIDBOpenDBRequest.prototype = Object.create(MockIDBRequest.prototype);
MockIDBOpenDBRequest.prototype.onblocked = null;
MockIDBOpenDBRequest.prototype.onupgradeneeded = null;
var MockIDBObjectStore = function(mOptions) {
this.name = mOptions.name;
this.keyPath = mOptions.keyPath;
this.indexNames = mOptions.indexNames;
this.transaction = mOptions.transaction;
this.autoIncrement = mOptions.autoIncrement || false;
};
MockIDBObjectStore.prototype.name = null;
MockIDBObjectStore.prototype.keyPath = null;
MockIDBObjectStore.prototype.indexNames = null;
MockIDBObjectStore.prototype.transaction = null;
MockIDBObjectStore.prototype.autoIncrement = false;
MockIDBObjectStore.prototype.put = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.add = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.delete = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.get = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.clear = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.openCursor = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.createIndex = function() {
throw new Error('Not implemented');
};
MockIDBObjectStore.prototype.index = function() {
throw new Error('Not implemented');
};
MockIDBObjectStore.prototype.deleteIndex = function() {
// void
};
MockIDBObjectStore.prototype.count = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
MockIDBObjectStore.prototype.mozGetAll = function() {
return new MockIDBRequest({
transaction: this.transaction,
store: this
});
};
var MockIDBTransaction = function(mOptions) {
this.db = mOptions.db;
this.mode = mOptions.mode || 'readonly';
this.objectStoreNames = mOptions.objectStoreNames;
};
MockIDBTransaction.prototype = new MockEventTarget();
MockIDBTransaction.prototype.mode = null;
MockIDBTransaction.prototype.db = null;
MockIDBTransaction.prototype.error = null;
MockIDBTransaction.prototype.onabort = null;
MockIDBTransaction.prototype.oncomplete = null;
MockIDBTransaction.prototype.onerror = null;
MockIDBTransaction.prototype.objectStoreNames = null;
MockIDBTransaction.prototype.objectStore = function(name) {
var mOptions = {
name: name,
transaction: this
};
if (name in this.db._objectStoreOptions) {
mOptions.keyPath = this.db._objectStoreOptions[name].keyPath;
mOptions.autoIncrement = this.db._objectStoreOptions[name].autoIncrement;
mOptions.indexNames = this.db._objectStoreOptions[name].indexNames;
}
return new MockIDBObjectStore(mOptions);
};
MockIDBTransaction.prototype.abort = function() {
// void
};
var MockIDBDatabase = function(mOptions) {
this.name = mOptions.name;
this.version = mOptions.version || 1;
this._objectStoreOptions = mOptions._objectStoreOptions || {};
this.objectStoreNames = mOptions.objectStoreNames;
};
MockIDBDatabase.prototype = new MockEventTarget();
MockIDBDatabase.prototype.name = null;
MockIDBDatabase.prototype.version = 0;
MockIDBDatabase.prototype.objectStoreNames = null;
MockIDBDatabase.prototype.onabort = null;
MockIDBDatabase.prototype.onerror = null;
MockIDBDatabase.prototype.onversionchange = null;
MockIDBDatabase.prototype.createObjectStore = function(name, params) {
return new MockIDBObjectStore({
name: name,
keyPath: params ? (params.keyPath || null) : null,
autoIncrement: params ? (params.autoIncrement || false) : false
});
};
MockIDBDatabase.prototype.deleteObjectStore = function(name) {
// void
};
MockIDBDatabase.prototype.transaction = function(storeNames, mode) {
return new MockIDBTransaction({
db: this,
objectStoreNames: storeNames,
mode: mode
});
};
MockIDBDatabase.prototype.close = function() {
// void
};
MockIDBDatabase.prototype.mozCreateFileHandle = function() {
throw new Error('Not implemented');
};
var MockIDBFactory = function() {
};
MockIDBFactory.attachToWindow = function(instance) {
if (!instance) {
instance = new this();
}
window.indexedDB._open = window.indexedDB.open;
window.indexedDB.open = instance.open.bind(instance);
window.indexedDB._cmp = window.indexedDB.cmp;
window.indexedDB.cmp = instance.cmp.bind(instance);
window.indexedDB._deleteDatabase = window.indexedDB.deleteDatabase;
window.indexedDB.deleteDatabase = instance.deleteDatabase.bind(instance);
};
MockIDBFactory.restore = function() {
window.indexedDB.open = window.indexedDB._open;
window.indexedDB.cmp = window.indexedDB._cmp;
window.indexedDB.deleteDatabase = window.indexedDB._deleteDatabase;
};
MockIDBFactory.prototype.cmp = function(first, second) {
// Should return a short number according to
// http://www.w3.org/TR/IndexedDB/
// #widl-IDBFactory-cmp-short-any-first-any-second
};
MockIDBFactory.prototype.open = function(name, version) {
// This DOMRequest should return a MockIDBDatabase instance.
return new MockIDBOpenDBRequest();
};
MockIDBFactory.prototype.deleteDatabase = function(name) {
return new MockIDBOpenDBRequest();
};
/*
exports.MockIDBFactory = MockIDBFactory;
exports.MockIDBOpenDBRequest = MockIDBOpenDBRequest;
exports.MockIDBDatabase = MockIDBDatabase;
exports.MockIDBObjectStore = MockIDBObjectStore;
exports.MockIDBTransaction = MockIDBTransaction;
exports.MockIDBRequest = MockIDBRequest;
})(window);
*/

View file

@ -0,0 +1,150 @@
'use strict';
/* global MockPromise */
require('/shared/test/unit/mocks/mock_promise.js');
suite('MockPromse', function() {
var state;
var resolveFn;
var rejectFn;
var resolveTo;
var rejectTo;
var p, p0, p1, p2, p3;
setup(function() {
this.sinon.spy(window, 'MockPromise');
p = new MockPromise(function callback(resolve, reject) {
state = 'callback';
resolveFn = resolve;
rejectFn = reject;
}).then(function(val) {
state = 'resolve1';
resolveTo = val;
}).then(function(val) {
state = 'resolve2';
resolveTo = val;
}).then(function(val) {
state = 'resolve3';
resolveTo = val;
}).catch(function(err) {
state = 'reject4';
rejectTo = err;
});
});
suite('callback', function() {
setup(function() {
p0 = window.MockPromise.firstCall.returnValue;
p0.mExecuteCallback(function resolve(val) {
state = 'resolve0';
resolveTo = val;
}, function reject(err) {
state = 'reject0';
rejectTo = err;
});
assert.equal(state, 'callback');
});
test('reject', function() {
var error = {};
rejectFn(error);
assert.equal(state, 'reject0');
assert.equal(rejectTo, error);
});
suite('resolve', function() {
var obj;
setup(function() {
obj = {};
resolveFn(obj);
});
test('resolve', function() {
assert.equal(state, 'resolve0');
assert.equal(resolveTo, obj);
});
test('reject1', function() {
var err1 = {};
p0.mRejectToError(err1);
assert.equal(state, 'reject4');
assert.equal(rejectTo, err1);
});
suite('resolve1', function() {
var obj1;
setup(function() {
obj1 = {};
p0.mFulfillToValue(obj1);
p1 = p0.mGetNextPromise();
});
test('resolve1', function() {
assert.equal(state, 'resolve1');
assert.equal(resolveTo, obj1);
});
test('reject2', function() {
var err2 = {};
p1.mRejectToError(err2);
assert.equal(state, 'reject4');
assert.equal(rejectTo, err2);
});
suite('resolve2', function() {
var obj2;
setup(function() {
obj2 = {};
p1.mFulfillToValue(obj2);
p2 = p1.mGetNextPromise();
});
test('resolve2', function() {
assert.equal(state, 'resolve2');
assert.equal(resolveTo, obj2);
});
test('reject3', function() {
var err3 = {};
p2.mRejectToError(err3);
assert.equal(state, 'reject4');
assert.equal(rejectTo, err3);
});
suite('resolve3', function() {
var obj3;
setup(function() {
obj3 = {};
p2.mFulfillToValue(obj3);
p3 = p2.mGetNextPromise();
});
test('resolve3', function() {
assert.equal(state, 'resolve3');
assert.equal(resolveTo, obj3);
var p4 = p3.catch.firstCall.returnValue;
assert.equal(p4, p);
});
test('reject4', function() {
var err4 = {};
p3.mRejectToError(err4);
assert.equal(state, 'reject4');
assert.equal(rejectTo, err4);
});
});
});
});
});
});
});

View file

@ -0,0 +1,85 @@
'use strict';
/* global BaseView */
require('/js/settings/base_view.js');
suite('BaseView', function() {
var view;
setup(function() {
var stubGetElementById = this.sinon.stub(document, 'getElementById');
view = new BaseView();
view.CONTAINER_ID = 'test-id';
view.start();
assert.isTrue(stubGetElementById.called);
});
teardown(function() {
view.stop();
});
suite('Transition event hooks', function() {
var stubPromiseAll;
setup(function() {
view.childViews = {
view1: {
beforeShow: this.sinon.stub().returns('beforeShow1'),
show: this.sinon.stub().returns('show1'),
beforeHide: this.sinon.stub().returns('beforeHide1'),
hide: this.sinon.stub().returns('hide1')
},
view2: {
beforeShow: this.sinon.stub().returns('beforeShow2'),
show: this.sinon.stub().returns('show2'),
beforeHide: this.sinon.stub().returns('beforeHide2'),
hide: this.sinon.stub().returns('hide2')
},
view3: {
beforeShow: this.sinon.stub().returns('beforeShow3'),
show: this.sinon.stub().returns('show3'),
beforeHide: this.sinon.stub().returns('beforeHide3'),
hide: this.sinon.stub().returns('hide3')
}
};
stubPromiseAll = this.sinon.stub(window.Promise, 'all');
});
test('beforeShow', function(){
var options = {};
view.beforeShow(options);
assert.isTrue(stubPromiseAll.calledWith(
['beforeShow1', 'beforeShow2', 'beforeShow3']));
assert.isTrue(view.childViews.view1.beforeShow.calledWith(options));
assert.isTrue(view.childViews.view2.beforeShow.calledWith(options));
assert.isTrue(view.childViews.view3.beforeShow.calledWith(options));
});
test('show', function(){
view.show();
assert.isTrue(stubPromiseAll.calledWith(['show1', 'show2', 'show3']));
});
test('beforeHide', function(){
view.beforeHide();
assert.isTrue(stubPromiseAll.calledWith(
['beforeHide1', 'beforeHide2', 'beforeHide3']));
});
test('hide', function(){
view.hide();
assert.isTrue(stubPromiseAll.calledWith(['hide1', 'hide2', 'hide3']));
});
});
});

View file

@ -0,0 +1,58 @@
'use strict';
/* global CloseLockManager */
require('/js/settings/close_locks.js');
suite('CloseLockManager', function() {
var manager;
setup(function() {
manager = new CloseLockManager();
manager.onclose = this.sinon.stub();
manager.start();
});
test('requestClose', function() {
manager.requestLock('requestClose');
assert.isTrue(manager.onclose.called);
});
suite('stayAwake', function() {
var awakeLock;
setup(function() {
awakeLock = manager.requestLock('stayAwake');
});
suite('requestClose', function() {
var closeLock;
setup(function() {
closeLock = manager.requestLock('requestClose');
assert.isFalse(manager.onclose.calledOnce);
});
test('unlock awakeLock', function() {
awakeLock.unlock();
assert.isTrue(manager.onclose.calledOnce);
});
test('unlock closeLock', function() {
closeLock.unlock();
assert.isFalse(manager.onclose.calledOnce);
awakeLock.unlock();
assert.isFalse(manager.onclose.calledOnce);
});
});
test('requestClose after unlock', function() {
awakeLock.unlock();
assert.isFalse(manager.onclose.calledOnce);
manager.requestLock('requestClose');
assert.isTrue(manager.onclose.calledOnce);
});
});
});

View file

@ -0,0 +1,138 @@
'use strict';
/* global BaseView, GeneralSettingsGroupView, HandwritingSettingsGroupView,
LayoutItemListView, GeneralPanel, PanelController, MockEventTarget */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/js/settings/base_view.js');
require('/js/settings/general_settings_view.js');
require('/js/settings/handwriting_settings_view.js');
require('/js/settings/layout_item_list_view.js');
require('/js/settings/general_panel.js');
require('/js/settings/panel_controller.js');
suite('General Panel', function() {
var panel;
var app;
var stubContainer;
var stubHeader;
var stubMenuUDItem;
var stubGetElemById;
var stubLayoutItemListView;
var stubGeneralSettingsGroupView;
var stubHandwritingSettingsGroupView;
setup(function() {
stubLayoutItemListView =
this.sinon.stub(Object.create(LayoutItemListView.prototype));
this.sinon.stub(window, 'LayoutItemListView')
.returns(stubLayoutItemListView);
stubGeneralSettingsGroupView =
this.sinon.stub(Object.create(GeneralSettingsGroupView.prototype));
this.sinon.stub(window, 'GeneralSettingsGroupView')
.returns(stubGeneralSettingsGroupView);
stubHandwritingSettingsGroupView =
this.sinon.stub(Object.create(HandwritingSettingsGroupView.prototype));
this.sinon.stub(window, 'HandwritingSettingsGroupView')
.returns(stubHandwritingSettingsGroupView);
app = {
panelController: this.sinon.stub(new PanelController()),
requestClose: this.sinon.stub()
};
panel = new GeneralPanel(app);
stubHeader = this.sinon.stub(new MockEventTarget());
stubContainer = {
querySelector:
this.sinon.stub().returns(stubHeader)
};
stubMenuUDItem = this.sinon.stub(new MockEventTarget());
stubGetElemById = this.sinon.stub(document, 'getElementById');
stubGetElemById
.withArgs('menu-userdict').returns(stubMenuUDItem)
.withArgs('general').returns(stubContainer);
panel.start();
assert.equal(panel.container, stubContainer);
assert.equal(panel._menuUDItem, stubMenuUDItem);
assert.isTrue(window.LayoutItemListView.calledWith(app));
assert.isTrue(window.GeneralSettingsGroupView.calledWith(app));
assert.isTrue(window.HandwritingSettingsGroupView.calledWith(app));
assert.isTrue(stubLayoutItemListView.start.called);
assert.isTrue(stubGeneralSettingsGroupView.start.called);
assert.isTrue(stubHandwritingSettingsGroupView.start.called);
});
teardown(function() {
panel.stop();
});
test('inheritance from BaseView', function() {
assert.instanceOf(panel, BaseView);
});
suite('Transition hooks', function() {
test('show', function() {
var stubBaseShow =
this.sinon.stub(BaseView.prototype, 'show').returns('ShowReturn');
var ret = panel.show();
assert.equal(ret, 'ShowReturn');
assert.isTrue(stubHeader.addEventListener.calledWith('action', panel));
assert.isTrue(stubMenuUDItem.addEventListener.calledWith('click', panel));
assert.isTrue(stubBaseShow.calledOn(panel));
});
test('beforeHide', function() {
var stubBaseBeforeHide =
this.sinon.stub(BaseView.prototype, 'beforeHide')
.returns('BeforeHideReturn');
var ret = panel.beforeHide();
assert.equal(ret, 'BeforeHideReturn');
assert.isTrue(stubHeader.removeEventListener.calledWith('action', panel));
assert.isTrue(
stubMenuUDItem.removeEventListener.calledWith('click', panel));
assert.isTrue(stubBaseBeforeHide.calledOn(panel));
});
});
suite('Event handling', function() {
test('action -> app.requstClose', function() {
panel.handleEvent({type: 'action'});
assert.isTrue(app.requestClose.called);
});
test('click -> navigate to user dictionary list panel', function() {
var spyPreventDefault = this.sinon.spy();
panel.handleEvent({
type: 'click',
preventDefault: spyPreventDefault
});
assert.isTrue(app.panelController.navigateToPanel.calledWith(
app.panelController.userDictionaryListPanel));
assert.isTrue(spyPreventDefault.called);
});
});
});

View file

@ -0,0 +1,61 @@
'use strict';
/* global SettingsView, GeneralSettingsGroupView, BaseView */
require('/js/settings/close_locks.js');
require('/js/settings/base_view.js');
require('/js/settings/settings_view.js');
require('/js/settings/general_settings_view.js');
suite('GeneralSettingsGroupView', function() {
var groupView;
var stubSettingsView;
var container;
var app;
setup(function() {
container = { stub: 'container' };
app = { stub: 'app' };
window.SoundFeedbackSettings = { stub: 'SoundFeedbackSettings' };
window.VibrationFeedbackSettings = { stub: 'VibrationFeedbackSettings' };
window.IMEngineSettings = { stub: 'IMEngineSettings' };
this.sinon.stub(document, 'getElementById').returns(container);
stubSettingsView = this.sinon.stub(Object.create(SettingsView.prototype));
this.sinon.stub(window, 'SettingsView').returns(stubSettingsView);
groupView = new GeneralSettingsGroupView(app);
});
test('inheritance of BaseView', function() {
assert.instanceOf(groupView, BaseView);
});
test('start/stop', function() {
groupView.start();
assert.isTrue(document.getElementById.calledWith(groupView.CONTAINER_ID));
assert.isTrue(window.SettingsView.calledWith(
app, container, window.SoundFeedbackSettings));
assert.isTrue(window.SettingsView.calledWith(
app, container, window.VibrationFeedbackSettings));
assert.isTrue(window.SettingsView.calledWith(
app, container, window.IMEngineSettings));
assert.isTrue(stubSettingsView.start.calledThrice);
assert.equal(groupView.childViews.soundFeedbackSettings, stubSettingsView);
assert.equal(groupView.childViews.vibrationFeedbackSettings,
stubSettingsView);
assert.equal(groupView.childViews.imEngineSettings, stubSettingsView);
groupView.stop();
assert.notProperty(groupView.childViews, 'soundFeedbackSettings');
assert.notProperty(groupView.childViews, 'vibrationFeedbackSettings');
assert.notProperty(groupView.childViews, 'imEngineSettings');
assert.isTrue(stubSettingsView.stop.calledThrice);
});
});

View file

@ -0,0 +1,50 @@
'use strict';
/* global SettingsView, HandwritingSettingsGroupView, BaseView */
require('/js/settings/close_locks.js');
require('/js/settings/base_view.js');
require('/js/settings/settings_view.js');
require('/js/settings/handwriting_settings_view.js');
suite('HandwritingSettingsGroupView', function() {
var groupView;
var stubSettingsView;
var container;
var app;
setup(function() {
container = { stub: 'container' };
app = { stub: 'app' };
window.HandwritingPadSettings = { stub: 'HandwritingPadSettings' };
this.sinon.stub(document, 'getElementById').returns(container);
stubSettingsView = this.sinon.stub(Object.create(SettingsView.prototype));
this.sinon.stub(window, 'SettingsView').returns(stubSettingsView);
groupView = new HandwritingSettingsGroupView(app);
});
test('inheritance of BaseView', function() {
assert.instanceOf(groupView, BaseView);
});
test('start/stop', function() {
groupView.start();
assert.isTrue(document.getElementById.calledWith(groupView.CONTAINER_ID));
assert.isTrue(window.SettingsView.calledWith(
app, container, window.HandwritingPadSettings));
assert.isTrue(stubSettingsView.start.calledOnce);
assert.equal(groupView.childViews.handwritingSettings, stubSettingsView);
groupView.stop();
assert.notProperty(groupView.childViews, 'handwritingSettings');
assert.isTrue(stubSettingsView.stop.calledOnce);
});
});

View file

@ -0,0 +1,155 @@
'use strict';
/* global KeyboardSettingsApp, PanelController, DialogController,
PromiseStorage */
require('/js/settings/close_locks.js');
require('/js/settings/base_view.js');
require('/js/settings/general_panel.js');
require('/js/settings/panel_controller.js');
require('/js/shared/promise_storage.js');
require('/js/settings/keyboard_settings_app.js');
suite('KeyboardSettingsApp', function() {
var app;
var realMozActivity;
var stubPanelController;
var stubDialogController;
var stubPerferencesStore;
var isHidden;
suiteSetup(function() {
Object.defineProperty(document, 'hidden', {
configurable: true,
get: function() {
return isHidden;
}
});
});
suiteTeardown(function() {
delete document.hidden;
});
setup(function() {
isHidden = false;
realMozActivity = window.MozActivity;
window.MozActivity = this.sinon.stub();
this.sinon.stub(window, 'close');
this.sinon.stub(document, 'addEventListener');
this.sinon.stub(document, 'removeEventListener');
window.SettingsPromiseManager = this.sinon.stub();
stubPanelController =
this.sinon.stub(Object.create(PanelController.prototype));
this.sinon.stub(window, 'PanelController')
.returns(stubPanelController);
stubDialogController =
this.sinon.stub(Object.create(DialogController.prototype));
this.sinon.stub(window, 'DialogController')
.returns(stubDialogController);
stubPerferencesStore =
this.sinon.stub(Object.create(PromiseStorage.prototype));
this.sinon.stub(window, 'PromiseStorage')
.returns(stubPerferencesStore);
app = new KeyboardSettingsApp();
});
teardown(function() {
window.MozActivity = realMozActivity;
});
suite('start', function() {
var skipStopOnTeardown;
setup(function() {
skipStopOnTeardown = false;
app.start();
assert.isTrue(window.SettingsPromiseManager.calledOnce);
assert.isTrue(stubPanelController.start.calledOnce);
assert.isTrue(window.PanelController.calledWith(app));
assert.isTrue(stubDialogController.start.calledOnce);
assert.isTrue(
window.PromiseStorage.calledWith(app.PREFERENCES_STORE_NAME));
assert.isTrue(stubPerferencesStore.start.calledOnce);
assert.isTrue(
document.addEventListener.calledWith('visibilitychange', app));
});
teardown(function() {
if (!skipStopOnTeardown) {
app.stop();
}
assert.isTrue(stubPanelController.stop.calledOnce);
assert.isTrue(stubDialogController.stop.calledOnce);
assert.isTrue(stubPerferencesStore.stop.calledOnce);
assert.isTrue(
document.removeEventListener.calledWith('visibilitychange', app));
});
test('requestClose', function() {
app.requestClose();
assert.isTrue(window.MozActivity.calledWith({
name: 'moz_configure_window',
data: { target: 'device' }
}));
});
test('visibilitychange to hidden', function() {
isHidden = true;
skipStopOnTeardown = true;
this.sinon.spy(app, 'stop');
app.handleEvent({
type: 'visibilitychange'
});
assert.isTrue(window.close.calledOnce);
assert.isTrue(app.stop.calledOnce);
});
test('visibilitychange to visible w/ stay awake lock', function() {
var lock = app.closeLockManager.requestLock('stayAwake');
isHidden = true;
this.sinon.spy(app, 'stop');
app.handleEvent({
type: 'visibilitychange'
});
assert.isFalse(window.close.calledOnce);
assert.isFalse(app.stop.calledOnce);
isHidden = false;
app.handleEvent({
type: 'visibilitychange'
});
assert.isFalse(window.close.calledOnce);
assert.isFalse(app.stop.calledOnce);
lock.unlock();
assert.isFalse(window.close.calledOnce);
assert.isFalse(app.stop.calledOnce);
});
});
});

View file

@ -0,0 +1,128 @@
'use strict';
/* global LayoutDictionaryDownloader */
require('/js/settings/layout_dictionary_downloader.js');
suite('LayoutDictionaryDownloader', function() {
var fakeXhr;
var requests;
var downloader;
var p;
var fakeLatinData = (function() {
var view = new Uint8Array(42);
var str = 'FxOSDICT';
var i = str.length;
while (i--) {
view[i] = str.charCodeAt(i);
}
return view.buffer;
})();
setup(function() {
requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
});
teardown(function() {
fakeXhr.restore();
});
test('onprogress', function() {
downloader = new LayoutDictionaryDownloader('latin', 'ab_cd.dict');
downloader.onprogress = this.sinon.stub();
p = downloader.load();
assert.equal(requests.length, 1);
var request = requests[0];
request.onprogress({
type: 'progress',
loaded: 10,
total: 100
});
assert.isTrue(downloader.onprogress.calledWith(10, 100));
});
test('abort()', function(done) {
downloader = new LayoutDictionaryDownloader('latin', 'ab_cd.dict');
downloader.onprogress = this.sinon.stub();
p = downloader.load();
downloader.abort();
var request = requests[0];
assert.isTrue(request.aborted);
p.then(function(data) {
assert.isTrue(false, 'Should not resolve.');
}, function() {
assert.isFalse(downloader.onprogress.calledOnce);
}).then(done, done);
});
suite('latin imEngine data', function() {
setup(function() {
downloader = new LayoutDictionaryDownloader('latin', 'ab_cd.dict');
downloader.onprogress = this.sinon.stub();
p = downloader.load();
assert.equal(requests.length, 1);
var request = requests[0];
assert.equal(request.url,
'https://fxos.cdn.mozilla.net/dictionaries/latin/1/ab_cd.dict');
assert.equal(request.responseType, 'arraybuffer');
});
test('load() with valid data.', function(done) {
var request = requests[0];
request.response = fakeLatinData;
request.respond(200, {}, '');
p.then(function(data) {
assert.isTrue(downloader.onprogress.calledWith(42, 42));
assert.equal(data, fakeLatinData);
}).then(done, done);
});
test('load() with invalid data.', function(done) {
var request = requests[0];
request.response = new ArrayBuffer(42);
request.respond(200, {}, '');
p.then(function(data) {
assert.isTrue(false, 'Should not resolve.');
}, function() {
assert.isFalse(downloader.onprogress.calledOnce);
}).then(done, done);
});
test('load() with xhr failure.', function(done) {
var request = requests[0];
request.response = null;
request.respond(0, {}, '');
p.then(function(data) {
assert.isTrue(false, 'Should not resolve.');
}, function() {
assert.isFalse(downloader.onprogress.calledOnce);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,285 @@
'use strict';
/* global LayoutDictionary, LayoutDictionaryList, PromiseStorage */
require('/js/settings/layout_dictionary_list.js');
require('/js/settings/layout_dictionary.js');
require('/js/shared/promise_storage.js');
suite('LayoutDictionaryList', function() {
var layoutListStub;
var layoutDicts;
var promiseStorageStub;
var list;
setup(function() {
layoutDicts = [];
var LayoutDictionaryPrototype = LayoutDictionary.prototype;
this.sinon.stub(window, 'LayoutDictionary', function() {
var layoutDict =
this.sinon.stub(Object.create(LayoutDictionaryPrototype));
layoutDicts.push(layoutDict);
return layoutDict;
}.bind(this));
var PromiseStoragePrototype = PromiseStorage.prototype;
promiseStorageStub =
this.sinon.stub(Object.create(PromiseStoragePrototype));
this.sinon.stub(window, 'PromiseStorage')
.returns(promiseStorageStub);
layoutListStub = {};
list = new LayoutDictionaryList(layoutListStub);
list.start();
assert.isTrue(window.PromiseStorage.calledWith(list.DATABASE_NAME));
assert.isTrue(promiseStorageStub.start.calledOnce);
});
teardown(function() {
list.stop();
layoutDicts.forEach(function(layoutDict) {
assert.isTrue(layoutDict.stop.calledOnce);
}, this);
});
test('getDictionary', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': true,
'preloaded': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
var layoutDict = layoutDicts[0];
assert.equal(list.getDictionary('latin', 'dictionaries/fr.dict'),
layoutDict);
});
suite('createDictionariesFromLayouts', function() {
test('ignore layout def w/o dictFilename', function() {
var layouts = [
{
'id': 'zh-Hans-Pinyin',
'name': '拼音',
'imEngineId': 'jspinyin',
'preloaded': true
}
];
list.createDictionariesFromLayouts(layouts);
assert.isFalse(window.LayoutDictionary.calledOnce);
});
test('Mark as preloaded', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': true,
'preloaded': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
assert.isTrue(window.LayoutDictionary.calledWith(list, {
filename: 'fr.dict',
imEngineId: 'latin',
filePath: 'dictionaries/fr.dict',
fileSize: 1874745,
databaseId: 'latin/dictionaries/fr.dict',
preloaded: true,
installedLayoutIds: new Set() // Must compare seperately
}));
var installedLayoutIds =
window.LayoutDictionary.getCall(0).args[1].installedLayoutIds;
assert.deepEqual(['fr'],
Array.from(installedLayoutIds).sort());
var layoutDict = layoutDicts[0];
assert.isTrue(layoutDict.start.calledOnce);
});
test('Mark as installed', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': true,
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
assert.isTrue(window.LayoutDictionary.calledWith(list, {
filename: 'fr.dict',
imEngineId: 'latin',
filePath: 'dictionaries/fr.dict',
fileSize: 1874745,
databaseId: 'latin/dictionaries/fr.dict',
preloaded: false,
installedLayoutIds: new Set() // Must compare seperately
}));
var installedLayoutIds =
window.LayoutDictionary.getCall(0).args[1].installedLayoutIds;
assert.deepEqual(['fr'],
Array.from(installedLayoutIds).sort());
var layoutDict = layoutDicts[0];
assert.isTrue(layoutDict.start.calledOnce);
});
test('Mark as not installed', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': false,
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
assert.isTrue(window.LayoutDictionary.calledWith(list, {
filename: 'fr.dict',
imEngineId: 'latin',
filePath: 'dictionaries/fr.dict',
fileSize: 1874745,
databaseId: 'latin/dictionaries/fr.dict',
preloaded: false,
installedLayoutIds: new Set() // Must compare seperately
}));
var installedLayoutIds =
window.LayoutDictionary.getCall(0).args[1].installedLayoutIds;
assert.deepEqual([],
Array.from(installedLayoutIds).sort());
var layoutDict = layoutDicts[0];
assert.isTrue(layoutDict.start.calledOnce);
});
test('Merge two layouts with same databaseId', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': true,
'preloaded': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr-CA',
'name': 'Français (Canadien)',
'imEngineId': 'latin',
'installed': true,
'preloaded': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
assert.isTrue(window.LayoutDictionary.calledWith(list, {
filename: 'fr.dict',
imEngineId: 'latin',
filePath: 'dictionaries/fr.dict',
fileSize: 1874745,
databaseId: 'latin/dictionaries/fr.dict',
preloaded: true,
installedLayoutIds: new Set() // Must compare seperately
}));
var installedLayoutIds =
window.LayoutDictionary.getCall(0).args[1].installedLayoutIds;
assert.deepEqual(['fr', 'fr-CA'],
Array.from(installedLayoutIds).sort());
var layoutDict = layoutDicts[0];
assert.isTrue(layoutDict.start.calledOnce);
});
test('Mark as installed if one layout is installed', function() {
var layouts = [
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'installed': false,
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr-CA',
'name': 'Français (Canadien)',
'imEngineId': 'latin',
'installed': true,
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
list.createDictionariesFromLayouts(layouts);
assert.isTrue(window.LayoutDictionary.calledWith(list, {
filename: 'fr.dict',
imEngineId: 'latin',
filePath: 'dictionaries/fr.dict',
fileSize: 1874745,
databaseId: 'latin/dictionaries/fr.dict',
preloaded: false,
installedLayoutIds: new Set() // Must compare seperately
}));
var installedLayoutIds =
window.LayoutDictionary.getCall(0).args[1].installedLayoutIds;
assert.deepEqual(['fr-CA'],
Array.from(installedLayoutIds).sort());
var layoutDict = layoutDicts[0];
assert.isTrue(layoutDict.start.calledOnce);
});
});
});

View file

@ -0,0 +1,597 @@
'use strict';
/* global LayoutDictionary, LayoutDictionaryDownloader, PromiseStorage */
require('/js/settings/layout_dictionary_downloader.js');
require('/js/settings/layout_dictionary.js');
require('/js/shared/promise_storage.js');
/*
* This unit test is organized to prove that we should be able to take
* another installForLayout() or removeForLayout() in whatever state we are in,
* except when the the dictionary is in preloaded state.
*/
suite('LayoutDictionary', function() {
var listStub;
var layoutDictionaryDownloaderStub;
var downloadDeferred;
var dbSetItemDeferred;
var dbDeleteItemDeferred;
var layoutDict;
var layoutItem;
var layoutItem2;
// Deferred is an object containing a promise with it's resolve/reject
// methods exposed.
// See
// https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules
// /Promise.jsm/Deferred
//
// For it's interfaces.
var Deferred = function() {
this.promise = new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
};
setup(function() {
layoutDictionaryDownloaderStub =
this.sinon.stub(Object.create(LayoutDictionaryDownloader.prototype));
this.sinon.stub(window, 'LayoutDictionaryDownloader')
.returns(layoutDictionaryDownloaderStub);
downloadDeferred = new Deferred();
layoutDictionaryDownloaderStub.load.returns(downloadDeferred.promise);
listStub = {
dbStore: this.sinon.stub(Object.create(PromiseStorage.prototype))
};
dbSetItemDeferred = new Deferred();
listStub.dbStore.setItem.returns(dbSetItemDeferred.promise);
dbDeleteItemDeferred = new Deferred();
listStub.dbStore.deleteItem.returns(dbDeleteItemDeferred.promise);
layoutItem = {
id: 'ab',
updateInstallProgress: this.sinon.stub()
};
layoutItem2 = {
id: 'cd',
updateInstallProgress: this.sinon.stub()
};
});
suite('STATE_PRELOADED', function() {
setup(function() {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: true,
installedLayoutIds: new Set(['ab'])
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
assert.equal(layoutDict.filename, 'bar.dict');
assert.equal(layoutDict.imEngineId, 'foo');
assert.equal(layoutDict.filePath, 'path/to/bar.dict');
assert.equal(layoutDict.fileSize, 100);
assert.equal(layoutDict.databaseId, 'foo/bar.dict');
assert.equal(layoutDict.installedLayoutIds, dict.installedLayoutIds);
assert.equal(layoutDict.state, layoutDict.STATE_PRELOADED);
});
test('installForLayout()', function() {
assert.throw(layoutDict.installForLayout.bind(layoutDict));
});
test('removeForLayout()', function() {
assert.throw(layoutDict.removeForLayout.bind(layoutDict));
});
});
suite('STATE_INSTALLABLE', function() {
setup(function() {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: false,
installedLayoutIds: new Set()
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
assert.equal(layoutDict.filename, 'bar.dict');
assert.equal(layoutDict.imEngineId, 'foo');
assert.equal(layoutDict.filePath, 'path/to/bar.dict');
assert.equal(layoutDict.fileSize, 100);
assert.equal(layoutDict.databaseId, 'foo/bar.dict');
assert.equal(layoutDict.installedLayoutIds, dict.installedLayoutIds);
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
});
test('installForLayout(layoutItem)', function() {
layoutDict.installForLayout(layoutItem);
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
assert.isTrue(layoutDictionaryDownloaderStub.load.calledOnce);
layoutDictionaryDownloaderStub.onprogress(10, 100);
assert.equal(layoutDict.downloadLoadedSize, 10);
assert.equal(layoutDict.downloadTotalSize, 100);
assert.isTrue(layoutItem.updateInstallProgress.calledWith(10, 100));
});
test('removeForLayout(layoutItem)', function(done) {
layoutItem = {
id: 'ab',
updateInstallProgress: this.sinon.stub()
};
var p = layoutDict.removeForLayout(layoutDict);
p.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.equal(layoutDict.downloadLoadedSize, 0);
assert.equal(layoutDict.downloadTotalSize, 0);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
});
suite('STATE_INSTALLING_CANCELLABLE', function() {
var p;
setup(function() {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: false,
installedLayoutIds: new Set()
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
p = layoutDict.installForLayout(layoutItem);
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
assert.isTrue(layoutDictionaryDownloaderStub.load.calledOnce);
});
test('Download fail', function(done) {
downloadDeferred.reject('mocked reject');
p.then(function() {
throw 'Should not resolve.';
}, function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
test('installForLayout(layoutItem)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem);
downloadDeferred.resolve({ data: 'data' });
downloadDeferred.promise
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING);
assert.isTrue(listStub.dbStore.setItem.calledWith(
layoutDict.databaseId, { data: 'data' }));
dbSetItemDeferred.resolve();
return p2;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
assert.equal(layoutDict.downloadLoadedSize, 0);
assert.equal(layoutDict.downloadTotalSize, 0);
})
.then(done, done);
});
// TODO: This case can be further expand to
// installForLayout(layoutItem2) -> fail to download etc.
// but here we only verify the two layouts are both marked as installed
// when installation is complete.
test('installForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem2);
layoutDictionaryDownloaderStub.onprogress(10, 100);
assert.equal(layoutDict.downloadLoadedSize, 10);
assert.equal(layoutDict.downloadTotalSize, 100);
assert.isTrue(layoutItem.updateInstallProgress.calledWith(10, 100));
assert.isTrue(layoutItem2.updateInstallProgress.calledWith(10, 100));
assert.isTrue(layoutDictionaryDownloaderStub.load.calledOnce);
downloadDeferred.resolve({ data: 'data' });
downloadDeferred.promise
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING);
assert.isTrue(listStub.dbStore.setItem.calledWith(
layoutDict.databaseId, { data: 'data' }));
dbSetItemDeferred.resolve();
return p2;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
assert.isTrue(layoutDict.installedLayoutIds.has('cd'));
assert.equal(layoutDict.downloadLoadedSize, 0);
assert.equal(layoutDict.downloadTotalSize, 0);
})
.then(done, done);
});
test('removeForLayout(layoutItem)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem);
assert.isTrue(layoutDictionaryDownloaderStub.abort.calledOnce);
// download is cancelled because of abort() here.
downloadDeferred.reject('mocked reject');
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
assert.isFalse(listStub.dbStore.setItem.calledOnce);
})
.then(done, done);
});
test('removeForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem2);
assert.isFalse(layoutDictionaryDownloaderStub.abort.calledOnce);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
test('removeForLayout(layoutItem2) (install two)', function(done) {
var pi_2 = layoutDict.installForLayout(layoutItem2);
var p2 = layoutDict.removeForLayout(layoutItem2);
assert.isFalse(layoutDictionaryDownloaderStub.abort.calledOnce);
p2.then(function() {
assert.equal(
layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
return pi_2;
}, function(e) {
throw e || 'Should not reject.';
})
.then(function() {
throw 'Should not resolve';
}, function() {
assert.isTrue(true, 'installForLayout promise rejected');
})
.then(done, done);
});
});
suite('STATE_INSTALLING', function() {
var p;
setup(function(done) {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: false,
installedLayoutIds: new Set()
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
p = layoutDict.installForLayout(layoutItem);
downloadDeferred.resolve({ data: 'data' });
downloadDeferred.promise
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING);
})
.then(done, done);
});
test('Install fail', function(done) {
dbSetItemDeferred.reject('mocked reject');
p.then(function() {
throw 'Should not resolve.';
}, function() {
return dbSetItemDeferred.promise.catch(function() {});
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
test('installForLayout(layoutItem)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem);
assert.isTrue(listStub.dbStore.setItem.calledWith(
layoutDict.databaseId, { data: 'data' }));
dbSetItemDeferred.resolve();
p2.then(function() {
return dbSetItemDeferred.promise;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
// TODO: This case can be further expand to
// installForLayout(layoutItem2) -> fail to install etc.
// but here we only verify the two layouts are both marked as installed
// when installation is complete.
test('installForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem2);
assert.isTrue(listStub.dbStore.setItem.calledWith(
layoutDict.databaseId, { data: 'data' }));
dbSetItemDeferred.resolve();
p2.then(function() {
return dbSetItemDeferred.promise;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
assert.isTrue(layoutDict.installedLayoutIds.has('cd'));
})
.then(done, done);
});
test('removeForLayout(layoutItem)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem);
assert.isTrue(listStub.dbStore.setItem.calledWith(
layoutDict.databaseId, { data: 'data' }));
dbSetItemDeferred.resolve();
p.then(function() {
throw 'Should not resolve.';
}, function() {
return dbSetItemDeferred.promise;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_REMOVING);
assert.isTrue(listStub.dbStore.deleteItem.calledWith(
layoutDict.databaseId));
dbDeleteItemDeferred.resolve();
return p2;
})
.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
test('removeForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
test('removeForLayout(layoutItem2) (installing two)', function(done) {
layoutDict.installForLayout(layoutItem2);
var p2 = layoutDict.removeForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
});
suite('STATE_INSTALLED', function() {
setup(function() {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: false,
installedLayoutIds: new Set(['ab'])
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
assert.equal(layoutDict.filename, 'bar.dict');
assert.equal(layoutDict.imEngineId, 'foo');
assert.equal(layoutDict.filePath, 'path/to/bar.dict');
assert.equal(layoutDict.fileSize, 100);
assert.equal(layoutDict.databaseId, 'foo/bar.dict');
assert.equal(layoutDict.installedLayoutIds, dict.installedLayoutIds);
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
});
test('installForLayout(layoutItem)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
test('installForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.installForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
assert.isTrue(layoutDict.installedLayoutIds.has('cd'));
})
.then(done, done);
});
test('removeForLayout(layoutItem)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem);
assert.equal(layoutDict.state, layoutDict.STATE_REMOVING);
assert.isTrue(listStub.dbStore.deleteItem.calledWith(
layoutDict.databaseId));
dbDeleteItemDeferred.resolve();
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
test('removeForLayout(layoutItem2) (w/ two layouts installed)',
function(done) {
layoutDict.installedLayoutIds.add('cd');
var p2 = layoutDict.removeForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
assert.isFalse(layoutDict.installedLayoutIds.has('cd'));
})
.then(done, done);
});
});
suite('STATE_REMOVING', function() {
var p;
setup(function() {
var dict = {
filename: 'bar.dict',
imEngineId: 'foo',
filePath: 'path/to/bar.dict',
fileSize: 100,
databaseId: 'foo/bar.dict',
preloaded: false,
installedLayoutIds: new Set(['ab'])
};
layoutDict = new LayoutDictionary(listStub, dict);
layoutDict.start();
p = layoutDict.removeForLayout(layoutItem);
assert.equal(layoutDict.state, layoutDict.STATE_REMOVING);
});
test('Removal fail', function(done) {
dbDeleteItemDeferred.reject('mocked reject');
p.then(function() {
throw 'Should not resolve.';
}, function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLED);
assert.isTrue(layoutDict.installedLayoutIds.has('ab'));
})
.then(done, done);
});
// TODO: This case can be further expand to
// installForLayout(layoutItem) -> fail to remove etc.
// but here we only verify the new layouts being installed when
// the removal is completed.
test('installForLayout(layoutItem)', function(done) {
layoutDict.installForLayout(layoutItem);
dbDeleteItemDeferred.resolve();
p.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
assert.isTrue(layoutDictionaryDownloaderStub.load.calledOnce);
})
.then(done, done);
});
// TODO: This case can be further expand to
// installForLayout(layoutItem2) -> fail to remove etc.
// but here we only verify the new layouts being installed when
// the removal is completed.
test('installForLayout(layoutItem2)', function(done) {
layoutDict.installForLayout(layoutItem2);
dbDeleteItemDeferred.resolve();
p.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_INSTALLING_CANCELLABLE);
assert.isFalse(layoutDict.installedLayoutIds.has('ab'));
assert.isTrue(layoutDictionaryDownloaderStub.load.calledOnce);
})
.then(done, done);
});
test('removeForLayout(layoutItem)', function() {
var p2 = layoutDict.removeForLayout(layoutItem);
assert.isTrue(p2 === p);
});
test('removeForLayout(layoutItem2)', function(done) {
var p2 = layoutDict.removeForLayout(layoutItem2);
p2.then(function() {
assert.equal(layoutDict.state, layoutDict.STATE_REMOVING);
}, function(e) {
throw e || 'Should not reject.';
})
.then(done, done);
});
});
});

View file

@ -0,0 +1,392 @@
'use strict';
/* global LayoutItem, LayoutDictionaryList, LayoutItemList, PromiseStorage,
SettingsPromiseManager */
require('/js/settings/layout_item.js');
require('/js/settings/layout_item_list.js');
require('/js/settings/layout_dictionary_list.js');
require('/js/shared/promise_storage.js');
require('/js/keyboard/settings.js');
suite('LayoutItemList', function() {
var layoutDictionaryListStub;
var dbGetItemConfigDeferred;
var dbSetItemDeferred;
var app;
var fakeXhr;
var list;
// Deferred is an object containing a promise with it's resolve/reject
// methods exposed.
// See
// https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules
// /Promise.jsm/Deferred
//
// For it's interfaces.
var Deferred = function() {
this.promise = new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
};
setup(function(done) {
layoutDictionaryListStub =
this.sinon.stub(Object.create(LayoutDictionaryList.prototype));
this.sinon.stub(window, 'LayoutDictionaryList')
.returns(layoutDictionaryListStub);
var LayoutItemPrototype = LayoutItem.prototype;
this.sinon.stub(window, 'LayoutItem', function() {
return this.sinon.stub(Object.create(LayoutItemPrototype));
}.bind(this));
var requests = [];
fakeXhr = sinon.useFakeXMLHttpRequest();
fakeXhr.onCreate = function(request) {
requests.push(request);
};
app = {
closeLockManager: { stub: 'closeLockManager' },
preferencesStore: sinon.stub(Object.create(PromiseStorage.prototype)),
settingsPromiseManager:
this.sinon.stub(Object.create(SettingsPromiseManager.prototype))
};
dbGetItemConfigDeferred = new Deferred();
app.preferencesStore.getItem
.withArgs(LayoutItemList.prototype.ENABLED_LAYOUT_KEY)
.returns(dbGetItemConfigDeferred.promise);
dbSetItemDeferred = new Deferred();
app.preferencesStore.setItem.returns(dbSetItemDeferred.promise);
list = new LayoutItemList(app);
list.onready = this.sinon.stub();
var p = list.start();
assert.equal(list.closeLockManager, app.closeLockManager);
assert.isTrue(window.LayoutDictionaryList.calledWith(list));
assert.isTrue(layoutDictionaryListStub.start.calledOnce);
assert.equal(list.dictionaryList, layoutDictionaryListStub);
assert.isTrue(
app.preferencesStore.getItem.calledWith(list.ENABLED_LAYOUT_KEY));
dbGetItemConfigDeferred.resolve(['fr']);
var request = requests[0];
assert.equal(request.url, list.CONFIG_FILE_PATH);
assert.equal(request.responseType, 'json');
request.response = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451390,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr-CA',
'name': 'Français (Canadien)',
'imEngineId': 'latin',
'preloaded': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
request.respond(200, {}, '');
p.then(done, done);
});
teardown(function() {
list.stop();
});
test('Create LayoutDictionary and LayoutItem instances', function() {
assert.isTrue(list.onready.calledOnce);
var layoutsWithInstallInfo = [
{
'id': 'en',
'name': 'English',
'imEngineId': 'latin',
'preloaded': true,
'installed': true,
'dictFilename': 'en_us.dict',
'dictFilePath': 'dictionaries/en_us.dict',
'dictFileSize': 1451390,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'preloaded': false,
'installed': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
},
{
'id': 'fr-CA',
'name': 'Français (Canadien)',
'imEngineId': 'latin',
'preloaded': false,
'installed': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
}
];
assert.isTrue(layoutDictionaryListStub.createDictionariesFromLayouts
.calledWith(layoutsWithInstallInfo));
layoutsWithInstallInfo.forEach(function(layout, i) {
assert.isTrue(window.LayoutItem.calledWith(list, layout));
var layoutItemStub = window.LayoutItem.getCall(i).returnValue;
assert.isTrue(layoutItemStub.start.calledOnce);
assert.equal(list.layoutItems.get(layout.id), layoutItemStub);
});
});
test('setLayoutAsInstalled', function(done) {
var p = list.setLayoutAsInstalled('fr-CA');
Promise.resolve()
.then(function() {
assert.isTrue(app.preferencesStore.setItem
.calledWith(list.ENABLED_LAYOUT_KEY, ['fr', 'fr-CA']));
dbSetItemDeferred.resolve();
return p;
})
.catch(function(e) {
throw (e || 'Should not reject.');
})
.then(done, done);
});
test('setLayoutAsUninstalled', function(done) {
var p = list.setLayoutAsUninstalled('fr');
Promise.resolve()
.then(function() {
assert.isTrue(app.preferencesStore.setItem
.calledWith(list.ENABLED_LAYOUT_KEY, []));
dbSetItemDeferred.resolve();
return p;
})
.catch(function(e) {
throw (e || 'Should not reject.');
})
.then(done, done);
});
suite('DownloadPreference', function() {
suite('getCurrentState', function() {
teardown(function() {
delete navigator.mozMobileConnections;
});
test('not using data connection', function(done) {
navigator.mozMobileConnections = {
'0': { data: { connected: false } },
'1': { data: { connected: false } },
length: 2 };
list.downloadPreference.getCurrentState().then(function(val) {
assert.equal(val, list.downloadPreference.STATE_ALLOW);
}).then(done, done);
});
test('data connection not avail', function(done) {
list.downloadPreference.getCurrentState().then(function(val) {
assert.equal(val, list.downloadPreference.STATE_ALLOW);
}).then(done, done);
});
suite('w/ data connection', function() {
var getItemDeferred;
setup(function() {
navigator.mozMobileConnections = {
'0': { data: { connected: true } },
'1': { data: { connected: false } },
length: 2 };
getItemDeferred = new Deferred();
app.preferencesStore.getItem.returns(getItemDeferred.promise);
});
test('preferences unset', function(done) {
getItemDeferred.resolve(undefined);
list.downloadPreference.getCurrentState().then(function(val) {
assert.isTrue(app.preferencesStore.getItem
.calledWith('download.prompt-on-data-connection'));
assert.equal(val, list.downloadPreference.STATE_PROMPT);
}).then(done, done);
});
test('preferences set to always allow', function(done) {
getItemDeferred.resolve(true);
list.downloadPreference.getCurrentState().then(function(val) {
assert.isTrue(app.preferencesStore.getItem
.calledWith('download.prompt-on-data-connection'));
assert.equal(val, list.downloadPreference.STATE_ALLOW);
}).then(done, done);
});
test('preferences set to always deny', function(done) {
getItemDeferred.resolve(false);
list.downloadPreference.getCurrentState().then(function(val) {
assert.isTrue(app.preferencesStore.getItem
.calledWith('download.prompt-on-data-connection'));
assert.equal(val, list.downloadPreference.STATE_DENY);
}).then(done, done);
});
test('preferences store rejects', function(done) {
getItemDeferred.reject('Mocked Error');
list.downloadPreference.getCurrentState().then(function(val) {
assert.isTrue(app.preferencesStore.getItem
.calledWith('download.prompt-on-data-connection'));
assert.equal(val, list.downloadPreference.STATE_PROMPT);
}).then(done, done);
});
});
});
suite('setDataConnectionDownloadState', function() {
var p;
setup(function() {
p = { stub: 'promise' };
app.preferencesStore.deleteItem.returns(p);
app.preferencesStore.setItem.returns(p);
});
test('set to prompt', function() {
var r = list.downloadPreference.setDataConnectionDownloadState(
list.downloadPreference.STATE_PROMPT);
assert.isTrue(app.preferencesStore.deleteItem.calledWith(
'download.prompt-on-data-connection'));
assert.equal(r, p);
});
test('set to allow', function() {
var r = list.downloadPreference.setDataConnectionDownloadState(
list.downloadPreference.STATE_ALLOW);
assert.isTrue(app.preferencesStore.setItem.calledWith(
'download.prompt-on-data-connection', true));
assert.equal(r, p);
});
test('set to deny', function() {
var r = list.downloadPreference.setDataConnectionDownloadState(
list.downloadPreference.STATE_DENY);
assert.isTrue(app.preferencesStore.setItem.calledWith(
'download.prompt-on-data-connection', false));
assert.equal(r, p);
});
});
suite('LayoutEnabler', function() {
var appManifestURL;
setup(function() {
appManifestURL =
location.protocol + '//' + location.hostname + '/manifest.webapp';
});
test('enableLayout', function(done) {
var deferred = new Deferred();
app.settingsPromiseManager.updateOne.returns(deferred.promise);
var p = list.layoutEnabler.enableLayout('foo');
assert.isTrue(app.settingsPromiseManager.updateOne.calledWith(
list.layoutEnabler.SETTING_ENABLE_LAYOUT_KEY));
var obj = {};
obj[appManifestURL] = {};
var refObj = {};
refObj[appManifestURL] = {};
refObj[appManifestURL].foo = true;
var returnedObj =
app.settingsPromiseManager.updateOne.firstCall.args[1](obj);
assert.deepEqual(returnedObj, refObj);
deferred.resolve();
p.catch(function(e) { throw e || 'Should not reject.'; })
.then(done, done);
});
test('disableLayout', function(done) {
var deferred = new Deferred();
app.settingsPromiseManager.updateOne.returns(deferred.promise);
var p = list.layoutEnabler.disableLayout('foo');
assert.isTrue(app.settingsPromiseManager.updateOne.calledWith(
list.layoutEnabler.SETTING_ENABLE_LAYOUT_KEY));
var obj = {};
obj[appManifestURL] = {};
obj[appManifestURL].foo = true;
obj[appManifestURL].bar = true;
var refObj = {};
refObj[appManifestURL] = {};
refObj[appManifestURL].bar = true;
var returnedObj =
app.settingsPromiseManager.updateOne.firstCall.args[1](obj);
assert.deepEqual(returnedObj, refObj);
deferred.resolve();
p.catch(function(e) { throw e || 'Should not reject.'; })
.then(done, done);
});
});
});
});

View file

@ -0,0 +1,463 @@
'use strict';
/* global LayoutItemListView, LayoutItemList, LayoutItemView,
DownloadPreference, LayoutEnabler */
require('/js/settings/base_view.js');
require('/js/settings/layout_item_list_view.js');
require('/js/settings/layout_item_list.js');
require('/js/settings/layout_item_view.js');
var Deferred = function() {
this.promise = new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
};
suite('LayoutItemListView', function() {
var listView;
var app;
var installedListElStub;
var installableListElStub;
var containerElStub;
var rememberMyChoiceElStub;
var layoutItemListStub;
var removeDialogElStub;
var downloadDialogElStub;
var toastElStub;
var enableDialogElStub;
var downloadPreferenceGetCurrentStateDeferred;
var downloadPreferenceSetDataConnectionDownloadStateDeferred;
setup(function() {
layoutItemListStub =
this.sinon.stub(LayoutItemList.prototype);
layoutItemListStub.downloadPreference =
this.sinon.stub(DownloadPreference.prototype);
layoutItemListStub.layoutEnabler =
this.sinon.stub(LayoutEnabler.prototype);
this.sinon.stub(window, 'LayoutItemList').returns(layoutItemListStub);
downloadPreferenceGetCurrentStateDeferred = new Deferred();
layoutItemListStub.downloadPreference.getCurrentState
.returns(downloadPreferenceGetCurrentStateDeferred.promise);
downloadPreferenceSetDataConnectionDownloadStateDeferred = new Deferred();
layoutItemListStub.downloadPreference.setDataConnectionDownloadState
.returns(
downloadPreferenceSetDataConnectionDownloadStateDeferred.promise);
var LayoutItemViewPrototype = LayoutItemView.prototype;
this.sinon.stub(window, 'LayoutItemView', function() {
var viewStub = this.sinon.stub(Object.create(LayoutItemViewPrototype));
viewStub.contaner = {};
viewStub.inList = viewStub.IN_LIST_INSTALLED;
return viewStub;
}.bind(this));
app = { stub: app };
containerElStub = { stub: 'container', hidden: true };
installedListElStub = {
appendChild: this.sinon.stub()
};
installableListElStub = {
appendChild: this.sinon.stub()
};
removeDialogElStub = document.createElement('div');
removeDialogElStub.appendChild(document.createElement('p'));
removeDialogElStub.hidden = true;
downloadDialogElStub = document.createElement('div');
downloadDialogElStub.hidden = true;
toastElStub = document.createElement('div');
toastElStub.show = this.sinon.stub();
toastElStub.hide = this.sinon.stub();
rememberMyChoiceElStub = document.createElement('input');
rememberMyChoiceElStub.type = 'checkbox';
enableDialogElStub = document.createElement('div');
enableDialogElStub.appendChild(document.createElement('p'));
enableDialogElStub.hidden = true;
listView = new LayoutItemListView(app);
this.sinon.stub(document, 'getElementById')
.withArgs(listView.CONTAINER_ID).returns(containerElStub)
.withArgs(listView.INSTALLED_LIST_ID).returns(installedListElStub)
.withArgs(listView.INSTALLABLE_LIST_ID).returns(installableListElStub)
.withArgs('installable-keyboards-removal-dialog')
.returns(removeDialogElStub)
.withArgs('installable-keyboards-download-error-toast')
.returns(toastElStub)
.withArgs('installable-keyboards-mobile-download-dialog')
.returns(downloadDialogElStub)
.withArgs('installable-keyboards-remember')
.returns(rememberMyChoiceElStub)
.withArgs('installable-keyboards-enable-dialog')
.returns(enableDialogElStub);
listView.start();
assert.isTrue(window.LayoutItemList.calledWith(app));
assert.isTrue(layoutItemListStub.start.calledOnce);
assert.equal(listView.container, containerElStub);
});
teardown(function() {
listView.stop();
assert.isTrue(layoutItemListStub.stop.calledOnce);
});
test('model.onready with 3 layoutItems', function() {
var items = layoutItemListStub.layoutItems = new Map();
items.set('foo', { id: 'foo' });
items.set('bar', { id: 'bar' });
items.set('baz', { id: 'baz' });
layoutItemListStub.onready();
assert.isFalse(containerElStub.hidden);
var i = 0;
items.forEach(function(item, id) {
assert.isTrue(
window.LayoutItemView.getCall(i).calledWith(listView, item));
var viewStub = window.LayoutItemView.getCall(i).returnValue;
assert.isTrue(viewStub.start.calledOnce);
assert.equal(installedListElStub.appendChild.getCall(i).args[0],
viewStub.container);
i++;
}, this);
});
test('Move one layoutItemView to installable', function() {
var items = layoutItemListStub.layoutItems = new Map();
items.set('foo', { id: 'foo' });
items.set('bar', { id: 'bar' });
items.set('baz', { id: 'baz' });
layoutItemListStub.onready();
assert.isFalse(containerElStub.hidden);
var viewStub0 = window.LayoutItemView.getCall(0).returnValue;
var viewStub1 = window.LayoutItemView.getCall(1).returnValue;
var viewStub2 = window.LayoutItemView.getCall(2).returnValue;
viewStub1.inList = viewStub1.IN_LIST_INSTALLABLE;
viewStub1.oninlistchange();
// The entire list of views must be go through in order.
assert.equal(installedListElStub.appendChild.getCall(3).args[0],
viewStub0.container);
assert.equal(installableListElStub.appendChild.getCall(0).args[0],
viewStub1.container);
assert.equal(installedListElStub.appendChild.getCall(4).args[0],
viewStub2.container);
});
test('model.onready with 0 layoutItems', function() {
layoutItemListStub.layoutItems = new Map();
layoutItemListStub.onready();
assert.isTrue(containerElStub.hidden);
assert.isFalse(window.LayoutItemView.calledOnce);
});
suite('confirmRemoval', function() {
var p;
setup(function() {
listView.beforeShow();
listView.show();
p = listView.confirmRemoval('Foo');
assert.isFalse(removeDialogElStub.hidden);
assert.equal(removeDialogElStub.firstElementChild.dataset.l10nArgs,
'{"keyboard":"Foo"}');
});
test('cancel', function(done) {
removeDialogElStub.dispatchEvent(new CustomEvent('cancel'));
p.then(function(val) {
assert.isTrue(removeDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
test('confirm', function(done) {
removeDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isTrue(removeDialogElStub.hidden);
assert.isTrue(val);
}).then(done, done);
});
});
suite('confirmDownload', function() {
var p;
setup(function() {
listView.beforeShow();
listView.show();
p = listView.confirmDownload('Foo');
assert.isTrue(
layoutItemListStub.downloadPreference.getCurrentState.calledOnce);
});
suite('STATE_PROMPT', function() {
setup(function(done) {
downloadPreferenceGetCurrentStateDeferred
.resolve(layoutItemListStub.downloadPreference.STATE_PROMPT);
downloadPreferenceGetCurrentStateDeferred.promise.then(function() {
assert.isFalse(downloadDialogElStub.hidden);
}).then(done, done);
});
suite('remember my choice checked', function() {
setup(function() {
rememberMyChoiceElStub.checked = true;
});
test('cancel', function(done) {
downloadDialogElStub.dispatchEvent(new CustomEvent('cancel'));
p.then(function(val) {
assert.isFalse(layoutItemListStub.downloadPreference
.setDataConnectionDownloadState.calledOnce,
'Should not remember cancelled dialog.');
assert.isTrue(downloadDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
test('confirm', function(done) {
downloadDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isTrue(layoutItemListStub.downloadPreference
.setDataConnectionDownloadState.calledWith(
layoutItemListStub.downloadPreference.STATE_ALLOW));
// Should not be blocked by setDataConnectionDownloadState promise.
assert.isTrue(downloadDialogElStub.hidden);
assert.isTrue(val);
// Resolve the promise.
downloadPreferenceSetDataConnectionDownloadStateDeferred.resolve();
return downloadPreferenceSetDataConnectionDownloadStateDeferred
.promise;
}).then(done, done);
});
});
suite('remember my choice not checked', function() {
setup(function() {
rememberMyChoiceElStub.checked = false;
});
test('cancel', function(done) {
downloadDialogElStub.dispatchEvent(new CustomEvent('cancel'));
p.then(function(val) {
assert.isFalse(layoutItemListStub.downloadPreference
.setDataConnectionDownloadState.calledOnce);
assert.isTrue(downloadDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
test('confirm', function(done) {
downloadDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isFalse(layoutItemListStub.downloadPreference
.setDataConnectionDownloadState.calledOnce);
assert.isTrue(downloadDialogElStub.hidden);
assert.isTrue(val);
}).then(done, done);
});
});
});
test('STATE_ALLOW', function(done) {
downloadPreferenceGetCurrentStateDeferred
.resolve(layoutItemListStub.downloadPreference.STATE_ALLOW);
p.then(function(val) {
assert.isTrue(downloadDialogElStub.hidden);
assert.isTrue(val);
}).then(done, done);
});
test('STATE_DENY', function(done) {
downloadPreferenceGetCurrentStateDeferred
.resolve(layoutItemListStub.downloadPreference.STATE_DENY);
p.then(function(val) {
assert.isTrue(downloadDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
});
suite('confirmEnable', function() {
var p;
test('queue two dialogs', function(done) {
assert.isTrue(enableDialogElStub.hidden);
var p = listView.confirmEnable('Foo');
var p2 = listView.confirmEnable('Bar');
assert.isTrue(enableDialogElStub.hidden);
listView.beforeShow();
listView.show();
assert.isFalse(enableDialogElStub.hidden);
assert.equal(enableDialogElStub.firstElementChild.dataset.l10nArgs,
'{"keyboard":"Foo"}');
// Confirm the first dialog
enableDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isTrue(val, 'Confirm the first dialog.');
assert.isFalse(enableDialogElStub.hidden, 'Dialog shown');
assert.equal(enableDialogElStub.firstElementChild.dataset.l10nArgs,
'{"keyboard":"Bar"}');
// Cancel the second dialog
enableDialogElStub.dispatchEvent(new CustomEvent('cancel'));
return p2;
})
.then(function(val) {
assert.isTrue(enableDialogElStub.hidden, 'Dialog hidden');
assert.isFalse(val, 'Cancel the second dialog');
})
.then(done, done);
});
suite('queue before visible', function() {
setup(function() {
p = listView.confirmEnable('Foo');
assert.isTrue(enableDialogElStub.hidden);
listView.beforeShow();
listView.show();
assert.isFalse(enableDialogElStub.hidden);
assert.equal(enableDialogElStub.firstElementChild.dataset.l10nArgs,
'{"keyboard":"Foo"}');
});
test('cancel', function(done) {
enableDialogElStub.dispatchEvent(new CustomEvent('cancel'));
p.then(function(val) {
assert.isTrue(enableDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
test('confirm', function(done) {
enableDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isTrue(enableDialogElStub.hidden);
assert.isTrue(val);
}).then(done, done);
});
});
suite('show while visible', function() {
setup(function() {
assert.isTrue(enableDialogElStub.hidden);
listView.beforeShow();
listView.show();
p = listView.confirmEnable('Foo');
assert.isFalse(enableDialogElStub.hidden);
assert.equal(enableDialogElStub.firstElementChild.dataset.l10nArgs,
'{"keyboard":"Foo"}');
});
test('cancel', function(done) {
enableDialogElStub.dispatchEvent(new CustomEvent('cancel'));
p.then(function(val) {
assert.isTrue(enableDialogElStub.hidden);
assert.isFalse(val);
}).then(done, done);
});
test('confirm', function(done) {
enableDialogElStub.dispatchEvent(new CustomEvent('confirm'));
p.then(function(val) {
assert.isTrue(enableDialogElStub.hidden);
assert.isTrue(val);
}).then(done, done);
});
});
});
test('disableLayout', function() {
layoutItemListStub.layoutEnabler.disableLayout.returns({ stub: 'promise' });
var p = listView.disableLayout('foo');
assert.deepEqual(p, { stub: 'promise' });
});
test('enableLayout', function() {
layoutItemListStub.layoutEnabler.enableLayout.returns({ stub: 'promise' });
var p = listView.enableLayout('foo');
assert.deepEqual(p, { stub: 'promise' });
});
suite('showDownloadErrorToast', function() {
test('call before the panel is shown', function() {
listView.showDownloadErrorToast();
assert.isFalse(toastElStub.show.calledOnce);
});
test('call after the panel is shown', function() {
listView.beforeShow();
listView.show();
listView.showDownloadErrorToast();
assert.isTrue(toastElStub.show.calledOnce);
});
test('hide the panel', function() {
listView.beforeHide();
listView.hide();
assert.isTrue(toastElStub.hide.calledOnce);
});
});
});

View file

@ -0,0 +1,548 @@
'use strict';
/* global LayoutDictionary, LayoutDictionaryList,
LayoutItemList, LayoutItem,
CloseLockManager, CloseLock,
MockInputMethod */
require('/js/settings/layout_item.js');
require('/js/settings/layout_item_list.js');
require('/js/settings/layout_dictionary.js');
require('/js/settings/layout_dictionary_list.js');
require('/js/settings/close_locks.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_navigator_input_method.js');
suite('LayoutItem', function() {
var item;
var layoutItemListStub;
var layoutDictionaryStub;
var setLayoutAsInstalledDeferred;
var setLayoutAsUninstalledDeferred;
var closeLockStub;
var installForLayoutDeferred;
var removeForLayoutDeferred;
var realMozInputMethod;
var addInputDeferred;
var removeInputDeferred;
// Deferred is an object containing a promise with it's resolve/reject
// methods exposed.
// See
// https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules
// /Promise.jsm/Deferred
//
// For it's interfaces.
var Deferred = function() {
this.promise = new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
};
setup(function() {
addInputDeferred = new Deferred();
removeInputDeferred = new Deferred();
realMozInputMethod = navigator.mozInputMethod;
navigator.mozInputMethod = new MockInputMethod();
this.sinon.stub(navigator.mozInputMethod, 'addInput')
.returns(addInputDeferred.promise);
this.sinon.stub(navigator.mozInputMethod, 'removeInput')
.returns(removeInputDeferred.promise);
installForLayoutDeferred = new Deferred();
removeForLayoutDeferred = new Deferred();
layoutDictionaryStub =
this.sinon.stub(Object.create(LayoutDictionary.prototype));
layoutDictionaryStub.installForLayout
.returns(installForLayoutDeferred.promise);
layoutDictionaryStub.removeForLayout
.returns(removeForLayoutDeferred.promise);
layoutItemListStub =
this.sinon.stub(Object.create(LayoutItemList.prototype));
layoutItemListStub.dictionaryList =
this.sinon.stub(Object.create(LayoutDictionaryList.prototype));
layoutItemListStub.dictionaryList
.getDictionary.returns(layoutDictionaryStub);
layoutItemListStub.closeLockManager =
this.sinon.stub(Object.create(CloseLockManager.prototype));
closeLockStub = this.sinon.stub(Object.create(CloseLock.prototype));
layoutItemListStub.closeLockManager.requestLock.returns(closeLockStub);
setLayoutAsInstalledDeferred = new Deferred();
setLayoutAsUninstalledDeferred = new Deferred();
layoutItemListStub.setLayoutAsInstalled
.returns(setLayoutAsInstalledDeferred.promise);
layoutItemListStub.setLayoutAsUninstalled
.returns(setLayoutAsUninstalledDeferred.promise);
});
teardown(function() {
item.stop();
});
test('Preloaded layout', function() {
var layout = {
'id': 'fr',
'name': 'Français',
'nameL10nId': 'french',
'imEngineId': 'latin',
'preloaded': true,
'installed': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
};
item = new LayoutItem(layoutItemListStub, layout);
item.start();
assert.equal(item.id, layout.id);
assert.equal(item.name, layout.name);
assert.equal(item.nameL10nId, layout.nameL10nId);
assert.equal(item.imEngineId, layout.imEngineId);
assert.equal(item.dictFilePath, layout.dictFilePath);
assert.equal(item.fileSize, layout.dictFileSize);
assert.deepEqual(item.types, layout.types);
assert.equal(item.state, item.STATE_PRELOADED);
assert.isFalse(layoutItemListStub.dictionaryList.getDictionary.calledOnce);
});
suite('Install', function() {
var p;
setup(function() {
var layout = {
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'preloaded': false,
'installed': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
};
item = new LayoutItem(layoutItemListStub, layout);
item.onstatechange = this.sinon.stub();
item.onprogress = this.sinon.stub();
item.oninstall = this.sinon.stub();
item.onerror = this.sinon.stub();
item.start();
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(layoutItemListStub.dictionaryList
.getDictionary.calledWith('latin', 'dictionaries/fr.dict'));
layoutDictionaryStub.state =
layoutDictionaryStub.STATE_INSTALLING_CANCELLABLE;
layoutDictionaryStub.downloadLoadedSize = 5;
layoutDictionaryStub.downloadTotalSize = 100;
p = item.install();
assert.isTrue(layoutItemListStub.closeLockManager
.requestLock.calledWith('stayAwake'));
});
test('Sucessful installation', function(done) {
Promise.resolve().then(function() {
assert.equal(
layoutDictionaryStub.installForLayout.firstCall.args[0], item);
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_INSTALLING_CANCELLABLE);
assert.equal(item.downloadLoadedSize, 5);
assert.equal(item.downloadTotalSize, 100);
layoutDictionaryStub.downloadLoadedSize = 10;
item.updateInstallProgress(10, 100);
assert.isTrue(item.onprogress.calledWith(10, 100));
assert.equal(item.downloadLoadedSize, 10);
assert.equal(item.downloadTotalSize, 100);
installForLayoutDeferred.resolve();
return installForLayoutDeferred.promise;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLING);
assert.equal(item.downloadLoadedSize, 0);
assert.equal(item.downloadTotalSize, 0);
assert.isTrue(navigator.mozInputMethod.addInput.calledWith('fr', {
launch_path: '/index.html#fr',
name: 'Français',
description: 'Français',
types: ['email', 'password', 'text', 'url']
}));
addInputDeferred.resolve();
return addInputDeferred.promise;
}).then(function() {
assert.isTrue(layoutItemListStub.setLayoutAsInstalled.calledWith('fr'));
setLayoutAsInstalledDeferred.resolve();
assert.isFalse(closeLockStub.unlock.calledOnce,
'Not called before setLayoutAsInstalled resolves');
return p;
}).then(function() {
assert.equal(item.onstatechange.callCount, 3);
assert.equal(item.state, item.STATE_INSTALLED);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isTrue(item.oninstall.calledOnce);
assert.isFalse(item.onerror.called);
}).then(done, done);
});
test('Dictionary install fail', function(done) {
Promise.resolve().then(function() {
assert.equal(
layoutDictionaryStub.installForLayout.firstCall.args[0], item);
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_INSTALLING_CANCELLABLE);
installForLayoutDeferred.reject('mocked reject');
return p;
}).then(function() {
throw 'Should not resolve.';
}, function(e) {
if (e && e !== 'mocked reject') { throw e; }
assert.isFalse(navigator.mozInputMethod.addInput.calledOnce);
assert.isFalse(layoutItemListStub.setLayoutAsInstalled.calledOnce);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(item.onerror.calledOnce);
assert.isFalse(item.oninstall.calledOnce);
assert.equal(
item.onerror.firstCall.args[0].error,
item.onerror.firstCall.args[0].ERROR_DOWNLOADERROR);
}).then(done, done);
});
test('Registration fail', function(done) {
Promise.resolve().then(function() {
assert.equal(
layoutDictionaryStub.installForLayout.firstCall.args[0], item);
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_INSTALLING_CANCELLABLE);
installForLayoutDeferred.resolve();
return installForLayoutDeferred.promise;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLING);
assert.isTrue(navigator.mozInputMethod.addInput.calledWith('fr', {
launch_path: '/index.html#fr',
name: 'Français',
description: 'Français',
types: ['email', 'password', 'text', 'url']
}));
addInputDeferred.reject('Failed');
return p;
}).then(function() {
throw 'Should not resolve.';
}, function(e) {
if (e && e !== 'Failed') { throw e; }
assert.isFalse(layoutItemListStub.setLayoutAsInstalled.calledOnce);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.equal(item.onstatechange.callCount, 3);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.equal(item.downloadLoadedSize, 0);
assert.equal(item.downloadTotalSize, 0);
assert.isTrue(item.onerror.calledOnce);
assert.isFalse(item.oninstall.calledOnce);
assert.equal(
item.onerror.firstCall.args[0].error,
item.onerror.firstCall.args[0].ERROR_INSTALLERROR);
}).then(done, done);
});
test('Set install fail', function(done) {
Promise.resolve().then(function() {
assert.equal(
layoutDictionaryStub.installForLayout.firstCall.args[0], item);
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_INSTALLING_CANCELLABLE);
installForLayoutDeferred.resolve();
return installForLayoutDeferred.promise;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLING);
assert.isTrue(navigator.mozInputMethod.addInput.calledWith('fr', {
launch_path: '/index.html#fr',
name: 'Français',
description: 'Français',
types: ['email', 'password', 'text', 'url']
}));
addInputDeferred.resolve();
return addInputDeferred.promise;
}).then(function() {
assert.isTrue(layoutItemListStub.setLayoutAsInstalled.calledWith('fr'));
setLayoutAsInstalledDeferred.reject('Failed');
return p;
}).then(function() {
throw 'Should not resolve.';
}, function(e) {
if (e && e !== 'Failed') { throw e; }
assert.equal(item.onstatechange.callCount, 3);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.equal(item.downloadLoadedSize, 0);
assert.equal(item.downloadTotalSize, 0);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isTrue(item.onerror.calledOnce);
assert.isFalse(item.oninstall.calledOnce);
assert.equal(
item.onerror.firstCall.args[0].error,
item.onerror.firstCall.args[0].ERROR_INSTALLERROR);
}).then(done, done);
});
});
suite('remove', function() {
var p;
setup(function() {
var layout = {
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'preloaded': false,
'installed': true,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
};
item = new LayoutItem(layoutItemListStub, layout);
item.onstatechange = this.sinon.stub();
item.onprogress = this.sinon.stub();
item.onerror = this.sinon.stub();
item.start();
assert.equal(item.state, item.STATE_INSTALLED);
assert.isTrue(layoutItemListStub.dictionaryList
.getDictionary.calledWith('latin', 'dictionaries/fr.dict'));
layoutDictionaryStub.state =
layoutDictionaryStub.STATE_INSTALLED;
p = item.remove();
assert.isTrue(layoutItemListStub.closeLockManager
.requestLock.calledWith('stayAwake'));
});
test('Sucessful removal', function(done) {
Promise.resolve().then(function() {
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_REMOVING);
assert.isTrue(navigator.mozInputMethod.removeInput.calledWith('fr'));
removeInputDeferred.resolve();
return removeInputDeferred.promise;
}).then(function() {
assert.isTrue(
layoutItemListStub.setLayoutAsUninstalled.calledWith('fr'));
setLayoutAsUninstalledDeferred.resolve();
return setLayoutAsUninstalledDeferred.promise;
}).then(function() {
assert.equal(
layoutDictionaryStub.removeForLayout.firstCall.args[0], item);
removeForLayoutDeferred.resolve();
assert.isFalse(closeLockStub.unlock.calledOnce,
'Not called before removeForLayout resolves');
return p;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isFalse(item.onerror.called);
}).then(done, done);
});
test('Registration fail', function(done) {
Promise.resolve().then(function() {
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_REMOVING);
assert.isTrue(navigator.mozInputMethod.removeInput.calledWith('fr'));
removeInputDeferred.reject('mocked reject');
return p;
}).then(function() {
throw 'Should not resolve.';
}, function() {
assert.isFalse(layoutDictionaryStub.removeForLayout.calledOnce);
assert.isFalse(layoutItemListStub.setLayoutAsUninstalled.calledOnce);
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLED);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isTrue(item.onerror.calledOnce);
assert.equal(
item.onerror.firstCall.args[0].error,
item.onerror.firstCall.args[0].ERROR_REMOVEERROR);
}).then(done, done);
});
test('Set uninstall fail', function(done) {
Promise.resolve().then(function() {
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_REMOVING);
assert.isTrue(navigator.mozInputMethod.removeInput.calledWith('fr'));
removeInputDeferred.resolve();
return removeInputDeferred.promise;
}).then(function() {
assert.isTrue(
layoutItemListStub.setLayoutAsUninstalled.calledWith('fr'));
setLayoutAsUninstalledDeferred.reject('Failed');
return setLayoutAsUninstalledDeferred.promise
.catch(function() {});
}).then(function() {
assert.equal(
layoutDictionaryStub.removeForLayout.firstCall.args[0], item);
removeForLayoutDeferred.resolve();
return p;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isFalse(item.onerror.called);
}).then(done, done);
});
test('Dictionary uninstall fail', function(done) {
Promise.resolve().then(function() {
assert.equal(item.onstatechange.callCount, 1);
assert.equal(item.state, item.STATE_REMOVING);
assert.isTrue(navigator.mozInputMethod.removeInput.calledWith('fr'));
removeInputDeferred.resolve();
return removeInputDeferred.promise;
}).then(function() {
assert.isTrue(
layoutItemListStub.setLayoutAsUninstalled.calledWith('fr'));
setLayoutAsUninstalledDeferred.resolve();
return setLayoutAsUninstalledDeferred.promise;
}).then(function() {
assert.equal(
layoutDictionaryStub.removeForLayout.firstCall.args[0], item);
removeForLayoutDeferred.reject('Failed');
return p;
}).then(function() {
assert.equal(item.onstatechange.callCount, 2);
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.isFalse(item.onerror.called);
}).then(done, done);
});
});
suite('cancelInstall', function() {
var p;
setup(function(done) {
var layout = {
'id': 'fr',
'name': 'Français',
'imEngineId': 'latin',
'preloaded': false,
'installed': false,
'dictFilename': 'fr.dict',
'dictFilePath': 'dictionaries/fr.dict',
'dictFileSize': 1874745,
'types': ['email', 'password', 'text', 'url']
};
item = new LayoutItem(layoutItemListStub, layout);
item.onstatechange = this.sinon.stub();
item.onprogress = this.sinon.stub();
item.onerror = this.sinon.stub();
item.start();
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(layoutItemListStub.dictionaryList
.getDictionary.calledWith('latin', 'dictionaries/fr.dict'));
layoutDictionaryStub.state =
layoutDictionaryStub.STATE_INSTALLING_CANCELLABLE;
p = item.install();
Promise.resolve().then(function() {
assert.equal(item.state, item.STATE_INSTALLING_CANCELLABLE);
}).then(done, done);
});
test('cancelInstall', function(done) {
item.cancelInstall();
assert.isTrue(layoutDictionaryStub.removeForLayout.calledWith(item));
removeForLayoutDeferred.resolve();
installForLayoutDeferred.reject('mocked reject');
p.then(function() {
throw 'Should not resolve';
}, function() {
assert.equal(item.state, item.STATE_INSTALLABLE);
assert.isTrue(closeLockStub.unlock.calledOnce);
assert.equal(item.downloadLoadedSize, 0);
assert.equal(item.downloadTotalSize, 0);
assert.isFalse(item.onerror.called);
})
.then(done, done);
});
});
});

View file

@ -0,0 +1,319 @@
'use strict';
/* global LayoutItemListView, LayoutItemView, LayoutItem, LayoutItemErrorInfo */
require('/js/settings/base_view.js');
require('/js/settings/layout_item.js');
require('/js/settings/layout_item_view.js');
require('/js/settings/layout_item_list_view.js');
var Deferred = function() {
this.promise = new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
};
suite('LayoutItemView', function() {
var view;
var itemStub;
var listViewStub;
var labelEl;
var statusEl;
var progressEl;
var confirmRemovalDeferred;
var confirmDownloadDeferred;
var confirmEnableDeferred;
var disableLayoutDeferred;
var enableLayoutDeferred;
setup(function() {
navigator.mozL10n = {
get: this.sinon.spy(function(id) { return 'l10n_get_' + id; })
};
itemStub = this.sinon.stub(LayoutItem.prototype);
itemStub.id = 'foo';
itemStub.name = 'Pig Latin';
itemStub.nameL10nId = 'pig-latin';
itemStub.fileSize = 24601;
itemStub.state = itemStub.STATE_PRELOADED;
confirmRemovalDeferred = new Deferred();
confirmDownloadDeferred = new Deferred();
confirmEnableDeferred = new Deferred();
disableLayoutDeferred = new Deferred();
enableLayoutDeferred = new Deferred();
listViewStub = this.sinon.stub(Object.create(LayoutItemListView.prototype));
listViewStub.confirmRemoval.returns(confirmRemovalDeferred.promise);
listViewStub.confirmDownload.returns(confirmDownloadDeferred.promise);
listViewStub.confirmEnable.returns(confirmEnableDeferred.promise);
listViewStub.disableLayout.returns(disableLayoutDeferred.promise);
listViewStub.enableLayout.returns(enableLayoutDeferred.promise);
view = new LayoutItemView(listViewStub, itemStub);
var templateEl = document.createElement('template');
templateEl.innerHTML =
'<div><p class="label"></p>' +
'<p><small class="status"></small></p>' +
'<progress class="progress" hidden></div>';
this.sinon.stub(document, 'getElementById')
.withArgs(view.TEMPLATE_ID).returns(templateEl);
view.oninlistchange = this.sinon.stub();
view.start();
labelEl = view.container.querySelector('.label');
statusEl = view.container.querySelector('.status');
progressEl = view.container.querySelector('.progress');
assert.equal(labelEl.textContent, 'Pig Latin');
assert.equal(labelEl.dataset.l10nId, 'pig-latin');
});
teardown(function() {
navigator.mozL10n = null;
});
test('STATE_PRELOADED', function() {
assert.equal(view.inList, view.IN_LIST_INSTALLED);
assert.equal(view.oninlistchange.callCount, 0);
assert.equal(view.container.dataset.enabledAction, 'none');
assert.equal(statusEl.dataset.l10nId, 'preInstalledStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"size":"24.02","sizeUnit":"l10n_get_byteUnit-KB"}');
assert.isTrue(progressEl.classList.contains('hide'));
});
test('STATE_INSTALLABLE', function() {
itemStub.state = itemStub.STATE_INSTALLABLE;
itemStub.onstatechange();
assert.equal(view.inList, view.IN_LIST_INSTALLABLE);
assert.equal(view.oninlistchange.callCount, 1);
assert.equal(view.container.dataset.enabledAction, 'download');
assert.equal(statusEl.dataset.l10nId, 'installableStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"size":"24.02","sizeUnit":"l10n_get_byteUnit-KB"}');
assert.isTrue(progressEl.classList.contains('hide'));
});
test('STATE_INSTALLING_CANCELLABLE', function() {
itemStub.state = itemStub.STATE_INSTALLING_CANCELLABLE;
itemStub.downloadLoadedSize = 1234;
itemStub.downloadTotalSize = 24601;
itemStub.onstatechange();
assert.equal(view.inList, view.IN_LIST_INSTALLABLE);
assert.equal(view.oninlistchange.callCount, 1);
assert.equal(view.container.dataset.enabledAction, 'cancel-download');
assert.equal(statusEl.dataset.l10nId, 'downloadingStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"loadedSize":"1.21","loadedSizeUnit":"l10n_get_byteUnit-KB",' +
'"totalSize":"24.02","totalSizeUnit":"l10n_get_byteUnit-KB"}');
assert.isFalse(progressEl.classList.contains('hide'));
assert.equal(progressEl.value, '1234');
assert.equal(progressEl.max, '24601');
itemStub.downloadLoadedSize = 2345;
itemStub.onprogress();
assert.equal(view.oninlistchange.callCount, 1);
assert.equal(view.container.dataset.enabledAction, 'cancel-download');
assert.equal(statusEl.dataset.l10nId, 'downloadingStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"loadedSize":"2.29","loadedSizeUnit":"l10n_get_byteUnit-KB",' +
'"totalSize":"24.02","totalSizeUnit":"l10n_get_byteUnit-KB"}');
assert.isFalse(progressEl.classList.contains('hide'));
assert.equal(progressEl.value, '2345');
assert.equal(progressEl.max, '24601');
});
test('STATE_INSTALLING', function() {
itemStub.state = itemStub.STATE_INSTALLING;
itemStub.onstatechange();
assert.equal(view.inList, view.IN_LIST_INSTALLABLE);
assert.equal(view.oninlistchange.callCount, 1);
assert.equal(view.container.dataset.enabledAction, 'none');
assert.equal(statusEl.dataset.l10nId, 'downloadingStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"loadedSize":"24.02","loadedSizeUnit":"l10n_get_byteUnit-KB",' +
'"totalSize":"24.02","totalSizeUnit":"l10n_get_byteUnit-KB"}');
assert.isFalse(progressEl.classList.contains('hide'));
assert.equal(progressEl.value, '24601');
assert.equal(progressEl.max, '24601');
});
test('STATE_INSTALLED', function() {
itemStub.state = itemStub.STATE_INSTALLED;
itemStub.onstatechange();
assert.equal(view.inList, view.IN_LIST_INSTALLED);
assert.equal(view.oninlistchange.callCount, 0);
assert.equal(view.container.dataset.enabledAction, 'remove');
assert.equal(statusEl.dataset.l10nId, 'installedStatus');
assert.equal(statusEl.dataset.l10nArgs,
'{"size":"24.02","sizeUnit":"l10n_get_byteUnit-KB"}');
assert.isTrue(progressEl.classList.contains('hide'));
});
test('STATE_REMOVING', function() {
itemStub.state = itemStub.STATE_REMOVING;
itemStub.onstatechange();
assert.equal(view.inList, view.IN_LIST_INSTALLED);
assert.equal(view.oninlistchange.callCount, 0);
assert.equal(view.container.dataset.enabledAction, 'none');
assert.equal(statusEl.dataset.l10nId, 'removingStatus');
assert.equal(statusEl.dataset.l10nArgs, undefined);
assert.isTrue(progressEl.classList.contains('hide'));
});
suite('handleEvent', function() {
suite('download', function() {
setup(function() {
var evt = {
type: 'click',
target: {
dataset: {
action: 'download'
}
}
};
view.handleEvent(evt);
assert.isTrue(listViewStub.confirmDownload.calledOnce);
});
test('confirmed', function(done) {
confirmDownloadDeferred.resolve(true);
confirmDownloadDeferred.promise.then(function() {
assert.isTrue(itemStub.install.calledOnce);
}).then(done, done);
});
test('cancelled', function(done) {
confirmDownloadDeferred.resolve(false);
confirmDownloadDeferred.promise.then(function() {
assert.isFalse(itemStub.install.calledOnce);
}).then(done, done);
});
});
test('cancelDownload', function() {
var evt = {
type: 'click',
target: {
dataset: {
action: 'cancelDownload'
}
}
};
view.handleEvent(evt);
assert.isTrue(itemStub.cancelInstall.calledOnce);
});
suite('remove', function() {
setup(function() {
var evt = {
type: 'click',
target: {
dataset: {
action: 'remove'
}
}
};
view.handleEvent(evt);
assert.isTrue(
listViewStub.confirmRemoval.calledWith('Pig Latin'));
});
test('confirmed', function(done) {
confirmRemovalDeferred.resolve(true);
confirmRemovalDeferred.promise.then(function() {
assert.isTrue(listViewStub.disableLayout.calledWith('foo'));
disableLayoutDeferred.resolve();
return disableLayoutDeferred.promise;
}).then(function() {
assert.isTrue(itemStub.remove.calledOnce);
}).then(done, done);
});
test('cancelled', function(done) {
confirmRemovalDeferred.resolve(false);
confirmRemovalDeferred.promise.then(function() {
assert.isFalse(listViewStub.disableLayout.calledOnce);
assert.isFalse(itemStub.remove.calledOnce);
}).then(done, done);
});
});
});
suite('oninstall', function() {
setup(function() {
itemStub.oninstall();
assert.isTrue(listViewStub.confirmEnable.calledWith('Pig Latin'));
});
test('confirmed', function(done) {
confirmEnableDeferred.resolve(true);
confirmEnableDeferred.promise.then(function() {
assert.isTrue(listViewStub.enableLayout.calledWith('foo'));
enableLayoutDeferred.resolve();
return enableLayoutDeferred.promise;
}).then(done, done);
});
test('cancelled', function(done) {
confirmEnableDeferred.resolve(false);
confirmEnableDeferred.promise.then(function() {
assert.isFalse(listViewStub.enableLayout.calledOnce);
}).then(done, done);
});
});
suite('onerror', function() {
test('ERROR_DOWNLOADERROR', function() {
var errorInfo = Object.create(LayoutItemErrorInfo.prototype);
errorInfo.error = errorInfo.ERROR_DOWNLOADERROR;
itemStub.onerror(errorInfo);
assert.isTrue(listViewStub.showDownloadErrorToast.calledOnce);
});
test('ERROR_INSTALLERROR', function() {
var errorInfo = Object.create(LayoutItemErrorInfo.prototype);
errorInfo.error = errorInfo.ERROR_INSTALLERROR;
itemStub.onerror(errorInfo);
assert.isFalse(listViewStub.showDownloadErrorToast.calledOnce);
});
test('ERROR_REMOVEERROR', function() {
var errorInfo = Object.create(LayoutItemErrorInfo.prototype);
errorInfo.error = errorInfo.ERROR_REMOVEERROR;
itemStub.onerror(errorInfo);
assert.isFalse(listViewStub.showDownloadErrorToast.calledOnce);
});
});
});

View file

@ -0,0 +1,334 @@
'use strict';
/* global PanelController, DialogController, MockPromise, BaseView,
GeneralPanel, UserDictionaryListPanel, UserDictionaryEditDialog */
require('/js/settings/base_view.js');
require('/js/settings/general_panel.js');
require('/js/settings/user_dictionary_list_panel.js');
require('/js/settings/user_dictionary_edit_dialog.js');
require('/js/settings/panel_controller.js');
require('/shared/test/unit/mocks/mock_promise.js');
suite('PanelController', function() {
var controller;
var stubGeneralPanel;
var stubUserDictionaryListPanel;
var app;
var panel;
var options;
var stubCreateTransitionPromise;
setup(function() {
stubGeneralPanel = this.sinon.stub(Object.create(GeneralPanel.prototype));
stubUserDictionaryListPanel =
this.sinon.stub(Object.create(UserDictionaryListPanel.prototype));
this.sinon.stub(window, 'UserDictionaryListPanel')
.returns(stubUserDictionaryListPanel);
panel = {
beforeShow: this.sinon.stub().returns('promisePBeforeShow'),
show: this.sinon.stub().returns('promisePShow'),
beforeHide: this.sinon.stub().returns('promisePBeforeHide'),
hide: this.sinon.stub().returns('promisePHide'),
container: document.createElement('div')
};
stubGeneralPanel.beforeShow.returns('promiseGBeforeShow');
stubGeneralPanel.show.returns('promiseGShow');
stubGeneralPanel.beforeHide.returns('promiseGBeforeHide');
stubGeneralPanel.hide.returns('promiseGHide');
app = {};
options = {};
this.sinon.stub(window, 'Promise', MockPromise);
this.sinon.spy(window.Promise, 'resolve');
controller = new PanelController(app);
this.sinon.stub(controller, 'RootPanelClass').returns(stubGeneralPanel);
controller.start();
// test for start()
assert.isTrue(stubGeneralPanel.start.called);
// test for subcomponent constructors
assert.isTrue(controller.RootPanelClass.calledWith(app));
// start(): Promise.resolve <----> 1st .then()
assert.isTrue(controller.rootPanel.beforeShow.calledOnce);
assert.isTrue(window.Promise.resolve.calledWith('promiseGBeforeShow'));
// start(): 1st .then() onwards
var p1 = window.Promise.resolve.firstCall.returnValue;
var val1 = p1.mFulfillToValue(undefined);
assert.equal(val1, 'promiseGShow');
assert.isTrue(controller.rootPanel.show.calledOnce);
// reset some stubbed calls so the actual test cases aren't affected
window.Promise.resolve.reset();
controller.rootPanel.beforeShow.reset();
controller.rootPanel.show.reset();
controller.rootPanel.container = document.createElement('div');
stubCreateTransitionPromise =
this.sinon.stub(controller, '_createTransitionPromise')
.returns('promiseTransition');
});
teardown(function() {
controller.stop();
assert.isTrue(stubUserDictionaryListPanel.stop.called);
assert.isTrue(stubGeneralPanel.stop.called);
});
test('alternative RootPanelClass', function(){
var AlternativeClass = function(){};
AlternativeClass.prototype =
this.sinon.stub(Object.create(BaseView.prototype));
var controller2 = new PanelController(app, AlternativeClass);
controller2.start(app);
assert.isTrue(AlternativeClass.prototype.start.called,
'start of AlternativeClass should be called');
});
test('navigateToPanel', function() {
controller.navigateToPanel(panel, options);
// Promise.resolve <----> 1st .then()
assert.isTrue(controller.rootPanel.beforeHide.called);
assert.isTrue(window.Promise.resolve.calledWith('promiseGBeforeHide'));
// 1st .then() <----> 2nd .then()
var p1 = window.Promise.resolve.firstCall.returnValue;
var val1 = p1.mFulfillToValue(undefined);
assert.isTrue(panel.beforeShow.calledWith(options));
assert.equal(val1, 'promisePBeforeShow');
// 2nd .then() <----> 3rd .then()
var p2 = p1.mGetNextPromise();
var val2 = p2.mFulfillToValue(undefined);
assert.equal(val2, 'promiseTransition');
assert.isTrue(stubCreateTransitionPromise.calledWith(panel.container));
assert.isTrue(panel.container.classList.contains('current'));
assert.isTrue(controller.rootPanel.container.classList.contains('prev'));
assert.isFalse(
controller.rootPanel.container.classList.contains('current'));
// 3rd .then() <----> 4th .then()
var p3 = p2.mGetNextPromise();
var val3 = p3.mFulfillToValue(undefined);
assert.isTrue(controller.rootPanel.hide.calledOnce);
assert.equal(val3, 'promiseGHide');
// 4th .then() onwards
var p4 = p3.mGetNextPromise();
var val4 = p4.mFulfillToValue(undefined);
assert.isTrue(panel.show.calledOnce);
assert.equal(val4, 'promisePShow');
});
test('navigateToRoot', function() {
controller._currentPanel = panel;
controller.navigateToRoot();
// Promise.resolve <----> 1st .then()
assert.isTrue(controller._currentPanel.beforeHide.called);
assert.isTrue(window.Promise.resolve.calledWith('promisePBeforeHide'));
// 1st .then() <----> 2nd .then()
var p1 = window.Promise.resolve.firstCall.returnValue;
var val1 = p1.mFulfillToValue(undefined);
assert.isTrue(controller.rootPanel.beforeShow.calledOnce);
assert.equal(val1, 'promiseGBeforeShow');
// 2nd .then() <----> 3rd .then()
var p2 = p1.mGetNextPromise();
var val2 = p2.mFulfillToValue(undefined);
assert.equal(val2, 'promiseTransition');
assert.isTrue(stubCreateTransitionPromise.calledWith(panel.container));
assert.isFalse(panel.container.classList.contains('current'));
assert.isFalse(controller.rootPanel.container.classList.contains('prev'));
assert.isTrue(
controller.rootPanel.container.classList.contains('current'));
// 3rd .then() <----> 4th .then()
var p3 = p2.mGetNextPromise();
var val3 = p3.mFulfillToValue(undefined);
assert.isTrue(controller._currentPanel.hide.calledOnce);
assert.equal(val3, 'promisePHide');
// 4th .then() onwards
var p4 = p3.mGetNextPromise();
var val4 = p4.mFulfillToValue(undefined);
assert.isTrue(controller.rootPanel.show.calledOnce);
assert.equal(val4, 'promiseGShow');
});
});
suite('DialogController', function() {
var stubUserDictionaryEditDialog;
var controller;
var dialog;
var options;
var stubCreateTransitionPromise;
setup(function() {
stubUserDictionaryEditDialog =
this.sinon.stub(Object.create(UserDictionaryEditDialog.prototype));
this.sinon.stub(window, 'UserDictionaryEditDialog')
.returns(stubUserDictionaryEditDialog);
dialog = {
beforeShow: this.sinon.stub().returns('promiseDBeforeShow'),
show: this.sinon.stub().returns('promiseDShow'),
beforeHide: this.sinon.stub().returns('promiseDBeforeHide'),
hide: this.sinon.stub().returns('promiseDHide'),
onsubmit: undefined,
container: document.createElement('div')
};
options = {};
this.sinon.stub(window, 'Promise', MockPromise);
this.sinon.spy(window.Promise, 'resolve');
controller = new DialogController();
controller.start();
stubCreateTransitionPromise =
this.sinon.stub(controller, '_createTransitionPromise')
.returns('promiseTransition');
});
teardown(function() {
controller.stop();
assert.isTrue(stubUserDictionaryEditDialog.stop.called);
});
suite('openDialog with a dialog', function() {
var resultPromise; // openDialog's returned Promise
var resResolve; // openDialog's internal resolve/reject func from Promise
var resReject;
setup(function() {
resultPromise = controller.openDialog(dialog, options);
var pResult = window.Promise.firstCall.returnValue;
resResolve = this.sinon.spy();
resReject = this.sinon.spy();
pResult.mExecuteCallback(resResolve, resReject);
});
test('normal flow', function() {
// Promise.resolve <----> 1st .then()
assert.isTrue(dialog.beforeShow.calledWith(options));
assert.isTrue(window.Promise.resolve.calledWith('promiseDBeforeShow'));
// 1st .then() <----> 2nd .then()
var p1 = window.Promise.resolve.firstCall.returnValue;
var val1 = p1.mFulfillToValue(undefined);
assert.equal(val1, 'promiseTransition');
assert.isTrue(stubCreateTransitionPromise.calledWith(dialog.container));
assert.isTrue(dialog.container.classList.contains('displayed'));
// 2nd .then() onwards
var p2 = p1.mGetNextPromise();
var val2 = p2.mFulfillToValue(undefined);
assert.isTrue(dialog.show.calledOnce);
assert.equal(val2, 'promiseDShow');
// inside onsubmit
stubCreateTransitionPromise.reset();
var results = {};
dialog.onsubmit(results);
assert.isTrue(resResolve.calledWith(results));
// Promise.resolve <-----> 1st .then(), inside onsubmit
assert.isTrue(
window.Promise.resolve.secondCall.calledWith('promiseDBeforeHide')
);
// 1st .then() <----> 2nd .then(), inside onsubmit
var pS1 = window.Promise.resolve.secondCall.returnValue;
var valS1 = pS1.mFulfillToValue(undefined);
assert.equal(valS1, 'promiseTransition');
assert.isTrue(stubCreateTransitionPromise.calledWith(dialog.container));
assert.isFalse(dialog.container.classList.contains('displayed'));
// 2nd .then() onwards, inside onsubmit
var pS2 = pS1.mGetNextPromise();
var valS2 = pS2.mFulfillToValue(undefined);
assert.strictEqual(dialog.onsubmit, undefined);
assert.equal(valS2, 'promiseDHide');
});
test('exception flow', function() {
// Promise.resolve <----> 1st .then()
var p1 = window.Promise.resolve.firstCall.returnValue;
var p2 = p1.mGetNextPromise();
p1.mRejectToError('error1');
assert.isTrue(resReject.calledWith('error1'));
resReject.reset();
p2.mRejectToError('error2');
assert.isTrue(resReject.calledWith('error2'));
});
});
test('openDialog argument not having onsubmit', function() {
this.sinon.spy(window.Promise, 'reject');
controller.openDialog({});
assert.isTrue(window.Promise.reject.calledWith(
'Dialog does not have a onsubmit callback'));
});
});

View file

@ -0,0 +1,171 @@
'use strict';
/* global SettingsManagerBase, SettingsPromiseManager, CloseLockManager,
CloseLock, SettingsView, BaseView */
require('/shared/js/component_utils.js');
require('/shared/elements/gaia_checkbox/script.js');
require('/js/settings/close_locks.js');
require('/js/settings/base_view.js');
require('/js/settings/settings_view.js');
require('/js/keyboard/settings.js');
suite('SettingsView', function() {
var app;
var view;
var stubSettings;
var el;
var el2;
setup(function(done) {
var container = document.createElement('div');
el = document.createElement('gaia-checkbox');
el.setAttribute('disabled', true);
el.dataset.setting = 'foo.bar';
container.appendChild(el);
el2 = document.createElement('input');
el2.type = 'range';
el2.disabled = true;
el2.value = 5;
el2.dataset.setting = 'foo.baz';
container.appendChild(el2);
this.sinon.spy(el, 'addEventListener');
this.sinon.spy(el2, 'addEventListener');
this.sinon.spy(el, 'removeEventListener');
this.sinon.spy(el2, 'removeEventListener');
stubSettings =
this.sinon.stub(Object.create(SettingsManagerBase.prototype));
stubSettings.initSettings.returns(Promise.resolve());
stubSettings.getSettingsSync.returns({
fooBar: true,
fooBaz: 10
});
var SettingsConstructor = this.sinon.stub().returns(stubSettings);
stubSettings.KEYS = ['foo.bar', 'foo.baz'];
stubSettings.PROPERTIES = ['fooBar', 'fooBaz'];
app = {
settingsPromiseManager:
this.sinon.stub(Object.create(SettingsPromiseManager.prototype)),
closeLockManager:
this.sinon.stub(Object.create(CloseLockManager.prototype))
};
app.closeLockManager.requestLock
.returns(this.sinon.stub(Object.create(CloseLock.prototype)));
view = new SettingsView(app, container, SettingsConstructor);
view.start();
assert.isTrue(el.hasAttribute('disabled'),
'Stay disabled until init settings returns');
assert.isTrue(el2.disabled, 'Stay disabled until init settings returns');
view.taskQueue.then(done, function() {
throw 'Most not reject';
}).catch(done);
});
teardown(function() {
view.stop();
});
test('inheritance from BaseView', function() {
assert.instanceOf(view, BaseView);
});
test('init settings returns', function() {
assert.isFalse(el.disabled, 'Enabled when init settings returns');
assert.isTrue(el.checked, 'Checked');
assert.isFalse(el2.disabled, 'Enabled when init settings returns');
assert.equal(el2.valueAsNumber, 10);
});
test('Update values', function(done) {
app.settingsPromiseManager.set.returns(Promise.resolve());
assert.isTrue(el.addEventListener.calledWith('change', view));
el.checked = false;
view.handleEvent({
target: el,
type: 'change'
});
assert.isTrue(el2.addEventListener.calledWith('change', view));
el2.value = 7;
view.handleEvent({
target: el2,
type: 'change'
});
assert.isTrue(el.disabled, 'Stay disabled until updates');
assert.isTrue(el2.disabled, 'Stay disabled until updates');
view.taskQueue.then(function() {
assert.isFalse(el.disabled);
assert.isFalse(el2.disabled);
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isTrue(app.closeLockManager.requestLock.firstCall
.returnValue.unlock.calledTwice);
assert.isTrue(
app.settingsPromiseManager.set.calledWith('foo.bar', false));
assert.isTrue(app.settingsPromiseManager.set.calledWith('foo.baz', 7));
assert.isFalse(el.checked, 'Checked');
assert.equal(el2.valueAsNumber, 7);
}, function() {
throw 'Most not reject';
}).then(done, done);
});
test('Update value failure', function(done) {
app.settingsPromiseManager.set.returns(Promise.reject('faked error'));
assert.isTrue(el2.addEventListener.calledWith('change', view));
el2.value = 7;
view.handleEvent({
target: el2,
type: 'change'
});
assert.isTrue(el2.disabled, 'Stay disabled until updates');
view.taskQueue.then(function() {
assert.isFalse(el2.disabled);
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isTrue(app.closeLockManager.requestLock.firstCall
.returnValue.unlock.calledOnce);
assert.isTrue(app.settingsPromiseManager.set.calledWith('foo.baz', 7));
assert.equal(el2.valueAsNumber, 10);
}, function() {
throw 'Most not reject';
}).then(done, done);
});
test('Handle remote settings change', function(done) {
var newValues = {
fooBar: false,
fooBaz: 7
};
stubSettings.getSettingsSync.returns(newValues);
stubSettings.onsettingchange(newValues);
view.taskQueue.then(function() {
assert.isFalse(el.checked, 'Checked');
assert.equal(el2.valueAsNumber, 7);
}, function() {
throw 'Most not reject';
}).then(done, done);
});
});

View file

@ -0,0 +1,228 @@
'use strict';
/* global UserDictionaryEditDialog, MockEventTarget, KeyEvent, BaseView */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/js/settings/base_view.js');
require('/js/settings/user_dictionary_edit_dialog.js');
suite('UserDictionary Edit Dialog', function() {
var dialog;
var stubContainer;
var stubGetElemById;
var stubHeader;
var stubSaveWordBtn;
var stubInput;
var stubDeleteBtn;
var stubDialogCancelBtn;
var stubDialogDeleteBtn;
setup(function() {
dialog = new UserDictionaryEditDialog();
stubContainer = {
querySelector:
this.sinon.stub().returns(() => this.sinon.stub(new MockEventTarget())),
classList: {
add: this.sinon.spy(),
remove: this.sinon.spy()
}
};
stubHeader = this.sinon.stub(new MockEventTarget());
stubSaveWordBtn = this.sinon.stub(new MockEventTarget());
stubInput = this.sinon.stub(new MockEventTarget());
stubDeleteBtn = this.sinon.stub(new MockEventTarget());
stubDialogCancelBtn = this.sinon.stub(new MockEventTarget());
stubDialogDeleteBtn = this.sinon.stub(new MockEventTarget());
stubGetElemById =
this.sinon.stub(document, 'getElementById').returns(stubContainer);
stubContainer.querySelector
.withArgs('#ud-editword-header').returns(stubHeader);
stubContainer.querySelector
.withArgs('#ud-saveword-btn').returns(stubSaveWordBtn);
stubContainer.querySelector
.withArgs('#ud-editword-input').returns(stubInput);
stubContainer.querySelector
.withArgs('#ud-editword-delete-btn').returns(stubDeleteBtn);
stubContainer.querySelector
.withArgs('#ud-editword-dialog-cancel-btn').returns(stubDialogCancelBtn);
stubContainer.querySelector
.withArgs('#ud-editword-dialog-delete-btn').returns(stubDialogDeleteBtn);
dialog.start();
assert.isTrue(stubGetElemById.calledWith('panel-ud-editword'));
assert.equal(dialog._inputField, stubInput);
});
teardown(function() {
dialog.stop();
});
test('inheritance from BaseView', function() {
assert.instanceOf(dialog, BaseView);
});
suite('Transition hooks', function() {
suite('beforeShow', function() {
test('edit mode', function() {
dialog.beforeShow({
word: 'star'
});
assert.isTrue(stubContainer.classList.remove.calledWith('add-mode'));
assert.equal(dialog._inputField.value, 'star');
assert.equal(dialog._oldWord, 'star');
});
test('add mode', function() {
dialog.beforeShow();
assert.isTrue(stubContainer.classList.add.calledWith('add-mode'));
assert.strictEqual(dialog._oldWord, undefined);
});
});
test('show', function() {
stubInput.focus = this.sinon.spy();
dialog.show();
assert.isTrue(stubHeader.addEventListener.calledWith('action', dialog));
assert.isTrue(
stubSaveWordBtn.addEventListener.calledWith('click', dialog));
assert.isTrue(stubInput.addEventListener.calledWith('keydown', dialog));
assert.isTrue(stubDeleteBtn.addEventListener.calledWith('click', dialog));
assert.isTrue(
stubDialogCancelBtn.addEventListener.calledWith('click', dialog));
assert.isTrue(
stubDialogDeleteBtn.addEventListener.calledWith('click', dialog));
assert.isTrue(stubInput.focus.called);
});
test('beforeHide', function() {
dialog.beforeHide();
assert.isTrue(
stubHeader.removeEventListener.calledWith('action', dialog));
assert.isTrue(
stubSaveWordBtn.removeEventListener.calledWith('click', dialog));
assert.isTrue(
stubInput.removeEventListener.calledWith('keydown', dialog));
assert.isTrue(
stubDeleteBtn.removeEventListener.calledWith('click', dialog));
assert.isTrue(
stubDialogCancelBtn.removeEventListener.calledWith('click', dialog));
assert.isTrue(
stubDialogDeleteBtn.removeEventListener.calledWith('click', dialog));
});
test('hide', function() {
dialog.hide();
assert.strictEqual(dialog._inputField.value, '');
assert.strictEqual(dialog._oldWord, undefined);
});
});
suite('Event handling', function() {
// this suite includes behavioral tests for _commitWord, _removeWord
// and _cancel
var stubDeleteDialog;
setup(function() {
dialog.onsubmit = this.sinon.spy();
stubDeleteDialog = {
setAttribute: this.sinon.spy(),
removeAttribute: this.sinon.spy()
};
stubContainer.querySelector
.withArgs('#ud-editword-delete-dialog').returns(stubDeleteDialog);
});
test('action -> cancel', function() {
dialog.handleEvent({type: 'action'});
assert.isTrue(dialog.onsubmit.calledWith({action: 'cancel'}));
});
test('enter key -> submit', function() {
stubInput.value = 'star';
stubInput.blur = this.sinon.spy();
dialog.handleEvent({type: 'keydown', keyCode: KeyEvent.DOM_VK_RETURN});
assert.isTrue(stubInput.blur.called);
assert.isTrue(
dialog.onsubmit.calledWith({action: 'commit', word: 'star'}));
});
test('save button -> submit', function() {
stubInput.value = 'star';
dialog.handleEvent({type: 'click', target: {id: 'ud-saveword-btn'}});
assert.isTrue(
dialog.onsubmit.calledWith({action: 'commit', word: 'star'}));
});
test('delete button -> show dialog', function() {
dialog._oldWord = 'oldstar';
var oldMozL10n = navigator.mozL10n;
navigator.mozL10n = {
setAttributes: this.sinon.spy()
};
stubContainer.querySelector
.withArgs('#ud-editword-delete-prompt').returns('dummy-delete');
dialog.handleEvent({
type: 'click',
target: {id: 'ud-editword-delete-btn'}
});
assert.isTrue(navigator.mozL10n.setAttributes.calledWith(
'dummy-delete',
'userDictionaryDeletePrompt',
{word: 'oldstar'}
));
assert.isTrue(stubDeleteDialog.removeAttribute.calledWith('hidden'));
navigator.mozL10n = oldMozL10n;
});
test('dialog delete button -> delete & hide dialog', function() {
dialog.handleEvent({
type: 'click',
target: {id: 'ud-editword-dialog-delete-btn'}
});
assert.isTrue(
dialog.onsubmit.calledWith({action: 'remove'}));
assert.isTrue(stubDeleteDialog.setAttribute.calledWith('hidden', true));
});
test('dialog cancel button -> hide dialog', function() {
dialog.handleEvent({
type: 'click',
target: {id: 'ud-editword-dialog-cancel-btn'}
});
assert.isFalse(
dialog.onsubmit.calledWith({action: 'remove'}));
assert.isTrue(stubDeleteDialog.setAttribute.calledWith('hidden', true));
});
});
});

View file

@ -0,0 +1,449 @@
'use strict';
/* global UserDictionaryListPanel, MockEventTarget, CloseLockManager, CloseLock,
PanelController, DialogController, UserDictionary, MockPromise,
BaseView */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_promise.js');
require('/js/settings/close_locks.js');
require('/js/settings/base_view.js');
require('/js/settings/general_panel.js');
require('/js/settings/panel_controller.js');
require('/js/settings/user_dictionary.js');
require('/js/settings/user_dictionary_list_panel.js');
suite('UserDictionary List Panel', function() {
var panel;
var app;
var stubUserDictionary;
var stubContainer;
var stubGetElemById;
var stubListContainer;
var stubHeader;
var stubAddWordBtn;
setup(function() {
stubUserDictionary =
this.sinon.stub(Object.create(UserDictionary.prototype));
this.sinon.stub(window, 'UserDictionary')
.returns(stubUserDictionary);
app = {
closeLockManager: this.sinon.stub(new CloseLockManager()),
panelController: this.sinon.stub(new PanelController()),
dialogController: this.sinon.stub(new DialogController()),
userDictionaryEditDialog: 'dummyEditDialog'
};
panel = new UserDictionaryListPanel(app);
stubContainer = {
querySelector:
this.sinon.stub().returns(() => this.sinon.stub(new MockEventTarget())),
classList: {
add: this.sinon.spy(),
remove: this.sinon.spy()
}
};
stubListContainer = this.sinon.stub(new MockEventTarget());
stubListContainer.appendChild = this.sinon.spy();
stubListContainer.removeChild = this.sinon.spy();
stubListContainer.childNodes = {};
stubHeader = this.sinon.stub(new MockEventTarget());
stubAddWordBtn = this.sinon.stub(new MockEventTarget());
stubGetElemById =
this.sinon.stub(document, 'getElementById').returns(stubContainer);
stubContainer.querySelector
.withArgs('#ud-wordlist-list').returns(stubListContainer);
stubContainer.querySelector
.withArgs('gaia-header').returns(stubHeader);
stubContainer.querySelector
.withArgs('#ud-addword-btn').returns(stubAddWordBtn);
panel.start();
assert.equal(panel.container, stubContainer);
assert.equal(panel._listContainer, stubListContainer);
assert.isTrue(stubUserDictionary.start.called);
});
teardown(function() {
panel.stop();
});
test('inheritance from BaseView', function() {
assert.instanceOf(panel, BaseView);
});
suite('Transition hooks', function() {
suite('beforeShow', function() {
var p;
setup(function(){
p = new MockPromise();
stubUserDictionary.getList.returns(p);
});
test('do not populate more than once', function() {
panel.beforeShow();
panel.beforeShow();
assert.isTrue(stubUserDictionary.getList.calledOnce);
});
suite('getList', function() {
test('empty Set', function(){
panel.beforeShow();
p.mFulfillToValue(new Set([]));
assert.isTrue(stubContainer.classList.add.calledWith('empty'));
});
test('Set with contents', function(){
var stubRearrangeList = this.sinon.stub(panel, '_rearrangeList');
panel.beforeShow();
p.mFulfillToValue(new Set(['star', 'stare', 'starry']));
assert.isTrue(stubContainer.classList.remove.calledWith('empty'));
assert.isTrue(
stubRearrangeList.calledWith(new Set(['star', 'stare', 'starry'])));
});
});
});
test('show', function() {
panel.show();
assert.isTrue(stubHeader.addEventListener.calledWith('action', panel));
assert.isTrue(stubAddWordBtn.addEventListener.calledWith('click', panel));
assert.isTrue(
stubListContainer.addEventListener.calledWith('click', panel));
});
test('beforeHide', function() {
panel.beforeHide();
assert.isTrue(stubHeader.removeEventListener.calledWith('action', panel));
assert.isTrue(
stubAddWordBtn.removeEventListener.calledWith('click', panel));
assert.isTrue(
stubListContainer.removeEventListener.calledWith('click', panel));
});
});
suite('Event handling', function() {
test('action -> navigate to root', function() {
panel.handleEvent({type: 'action'});
assert.isTrue(app.panelController.navigateToRoot.called);
});
test('click on addWordButton -> showAddDialog', function() {
var stubShowDialog = this.sinon.stub(panel, '_showAddDialog');
panel.handleEvent({type: 'click', target: {id: 'ud-addword-btn'}});
assert.isTrue(stubShowDialog.called);
});
test('click on word -> showEditDialog', function() {
var stubShowDialog = this.sinon.stub(panel, '_showEditDialog');
var target = {tagName: 'LI'};
var spyPreventDefault = this.sinon.spy();
panel.handleEvent({
type: 'click',
target: target,
preventDefault: spyPreventDefault
});
assert.isTrue(stubShowDialog.calledWith(target));
assert.isTrue(spyPreventDefault.called);
});
});
test('rearrangeList', function() {
panel._wordDomMap = {
star: {t: 'star'},
starry: {t: 'starry'}
};
var stubStareWordElem = {cT: 'stare'};
this.sinon.stub(panel, '_createWordElem').returns(stubStareWordElem);
panel._rearrangeList(['star', 'stare', 'starry']);
// as the order of the words in the list is important, we need to make sure
// appendChild calls are in the same order.
assert.isTrue(stubListContainer.appendChild.getCall(0)
.calledWith(panel._wordDomMap.star));
assert.isTrue(stubListContainer.appendChild.getCall(1)
.calledWith(stubStareWordElem));
assert.isTrue(stubListContainer.appendChild.getCall(2)
.calledWith(panel._wordDomMap.starry));
});
test('createWordElem', function() {
var stubA = {};
var stubLI = {
appendChild: this.sinon.spy()
};
var stubCreateElem = this.sinon.stub(document, 'createElement');
stubCreateElem.withArgs('a').returns(stubA);
stubCreateElem.withArgs('li').returns(stubLI);
var ret = panel._createWordElem('star');
assert.equal(stubLI, ret);
assert.equal(stubA.textContent, 'star');
assert.equal(stubA.href, '#star');
assert.isTrue(stubLI.appendChild.calledWith(stubA));
});
suite('showAddDialog', function() {
// this suite tests addWord behavior too.
var pDialog;
setup(function() {
pDialog = new MockPromise();
app.dialogController.openDialog.returns(pDialog);
});
test('canceled dialog', function() {
var stubAddWord = this.sinon.stub(panel, '_addWord');
panel._showAddDialog();
assert.isTrue(app.dialogController.openDialog.calledWith(
app.dialogController.userDictionaryEditDialog));
pDialog.mFulfillToValue({action: 'cancel'});
assert.isFalse(stubAddWord.called);
});
suite('submitted dialog', function() {
var pAdd;
var closeLock;
setup(function(){
pAdd = new MockPromise();
closeLock = this.sinon.stub(new CloseLock());
app.closeLockManager.requestLock.returns(closeLock);
stubUserDictionary.addWord.returns(pAdd);
panel._showAddDialog();
});
test('white space only (no effect)', function(){
pDialog.mFulfillToValue({action: 'commit', word: ' \t '});
assert.isFalse(app.closeLockManager.requestLock.called);
assert.isFalse(stubUserDictionary.addWord.called);
});
test('successful', function(){
var stubRearrangeList= this.sinon.stub(panel, '_rearrangeList');
pDialog.mFulfillToValue({action: 'commit', word: 'star'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isTrue(stubUserDictionary.addWord.calledWith('star'));
pAdd.mFulfillToValue(['star']);
assert.isTrue(closeLock.unlock.called);
assert.isTrue(stubContainer.classList.remove.calledWith('empty'));
assert.isTrue(stubRearrangeList.calledWith(['star']));
});
test('still unlocks CloseLock after rejection from model', function(){
pDialog.mFulfillToValue({action: 'commit', word: 'star'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
pAdd.mRejectToError();
assert.isTrue(closeLock.unlock.called);
});
});
});
suite('showEditDialog', function() {
// this suite tests removeWord & replaceWord behavior too.
var pDialog;
var stubWordElem;
setup(function() {
pDialog = new MockPromise();
stubWordElem = {
childNodes: [{
textContent: 'star'
}]
};
panel._domWordMap.set(stubWordElem, 'star');
app.dialogController.openDialog.returns(pDialog);
});
test('word is correctly propogated to dialog', function() {
panel._showEditDialog(stubWordElem);
assert.isTrue(app.dialogController.openDialog.calledWith(
app.dialogController.userDictionaryEditDialog, {word: 'star'}));
});
test('canceled dialog', function() {
var stubRemoveWord = this.sinon.stub(panel, '_removeWord');
var stubReplaceWord = this.sinon.stub(panel, '_replaceWord');
panel._showEditDialog(stubWordElem);
pDialog.mFulfillToValue({action: 'cancel'});
assert.isFalse(stubReplaceWord.called);
assert.isFalse(stubRemoveWord.called);
});
suite('submitted dialog for remove', function() {
var pRemove;
var closeLock;
setup(function(){
pRemove = new MockPromise();
closeLock = this.sinon.stub(new CloseLock());
app.closeLockManager.requestLock.returns(closeLock);
stubUserDictionary.removeWord.returns(pRemove);
panel._showEditDialog(stubWordElem);
});
test('successful, list not empty yet', function(){
stubListContainer.childNodes.length = 2;
pDialog.mFulfillToValue({action: 'remove'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isTrue(stubUserDictionary.removeWord.calledWith('star'));
pRemove.mFulfillToValue(undefined);
assert.isTrue(closeLock.unlock.called);
assert.isFalse(panel._domWordMap.has(stubWordElem));
assert.isTrue(stubListContainer.removeChild.calledWith(stubWordElem));
assert.isFalse(stubContainer.classList.add.called);
});
test('successful, list empty (partial test)', function(){
stubListContainer.childNodes.length = 0;
pDialog.mFulfillToValue({action: 'remove'});
pRemove.mFulfillToValue(undefined);
assert.isTrue(stubContainer.classList.add.calledWith('empty'));
});
test('still unlocks CloseLock after rejection from model', function(){
pDialog.mFulfillToValue({action: 'remove'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
pRemove.mRejectToError();
assert.isTrue(closeLock.unlock.called);
});
});
suite('submitted dialog for replace', function() {
var pReplace;
var closeLock;
setup(function(){
pReplace = new MockPromise();
closeLock = this.sinon.stub(new CloseLock());
app.closeLockManager.requestLock.returns(closeLock);
stubUserDictionary.updateWord.returns(pReplace);
panel._showEditDialog(stubWordElem);
});
test('newWord is empty (do nothing)', function(){
pDialog.mFulfillToValue({action: 'commit', word: ' \t '});
assert.isFalse(
app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isFalse(stubUserDictionary.updateWord.called);
});
test('newWord == oldWord (do nothing)', function(){
pDialog.mFulfillToValue({action: 'commit', word: 'star'});
assert.isFalse(
app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isFalse(stubUserDictionary.updateWord.called);
});
test('successful, new word does not exist yet', function(){
var stubRearrangeList= this.sinon.stub(panel, '_rearrangeList');
pDialog.mFulfillToValue({action: 'commit', word: 'star2'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
assert.isTrue(
stubUserDictionary.updateWord.calledWith('star', 'star2'));
pReplace.mFulfillToValue(['star2']);
assert.isTrue(closeLock.unlock.called);
assert.equal(panel._domWordMap.get(stubWordElem), 'star2');
assert.equal(stubWordElem.childNodes[0].textContent, 'star2');
assert.isTrue(stubRearrangeList.calledWith(['star2']));
});
test('successful, new word already exists', function(){
pDialog.mFulfillToValue({action: 'commit', word: 'star2'});
pReplace.mRejectToError('existing');
assert.isFalse(panel._domWordMap.has(stubWordElem));
assert.isTrue(stubListContainer.removeChild.calledWith(stubWordElem));
});
test('still unlocks CloseLock after rejection from model', function(){
pDialog.mFulfillToValue({action: 'commit', word: 'star2'});
assert.isTrue(app.closeLockManager.requestLock.calledWith('stayAwake'));
pReplace.mRejectToError();
assert.isTrue(closeLock.unlock.called);
});
});
});
});

View file

@ -0,0 +1,289 @@
'use strict';
/* global MockPromise, PromiseStorage, UserDictionary, WordListConverter */
require('/shared/test/unit/mocks/mock_promise.js');
require('/js/shared/promise_storage.js');
require('/js/settings/word_list_converter.js');
require('/js/settings/user_dictionary.js');
suite('UserDictionary', function() {
var model;
var stubPromiseStorage;
var stubWordListConverter;
// Note in comparing against Set(): deepEqual won't work like
// deepEqual(set1, set2).
// Please use deepEqual(Array.from(set1), Array.from(set2)).
setup(function() {
model = new UserDictionary();
this.sinon.stub(window, 'Promise', MockPromise);
stubPromiseStorage =
this.sinon.stub(Object.create(PromiseStorage.prototype));
this.sinon.stub(window, 'PromiseStorage')
.returns(stubPromiseStorage);
stubWordListConverter =
this.sinon.stub(Object.create(WordListConverter.prototype));
this.sinon.stub(window, 'WordListConverter')
.returns(stubWordListConverter);
model.start();
assert.isTrue(stubPromiseStorage.start.calledOnce);
});
teardown(function() {
model.stop();
assert.isTrue(stubPromiseStorage.stop.calledOnce);
});
suite('getList', function() {
var pDB;
var pRet;
setup(function() {
pDB = new MockPromise();
model._dbStore.getItem.returns(pDB);
pRet = model.getList();
assert.isTrue(model._dbStore.getItem.calledWith('wordlist'));
// see if the returned-by-getList promise depends on dbStore's
// promise.
assert.equal(pRet,
pDB.then.firstCall.returnValue.catch.firstCall.returnValue);
});
test('normal result', function(){
var ret = pDB.mFulfillToValue(['star', 'stare', 'starry']);
assert.deepEqual(Array.from(ret), ['star', 'stare', 'starry']);
});
test('undefined result = empty list', function(){
var ret = pDB.mFulfillToValue(undefined);
assert.deepEqual(Array.from(ret), []);
});
test('empty result = empty list', function(){
var ret = pDB.mFulfillToValue([]);
assert.deepEqual(Array.from(ret), []);
});
test('promise rejected result = empty list with console log', function(){
this.sinon.stub(window.console, 'error');
var ret = pDB.mRejectToError('errorInDB');
assert.deepEqual(Array.from(ret), []);
assert.isTrue(window.console.error.calledOnce);
});
});
suite('addWord', function() {
var pSaveList;
setup(function() {
pSaveList = new MockPromise();
this.sinon.stub(model, '_saveDict').returns(pSaveList);
model._wordSet = new Set();
});
test('normal case', function(){
var pRet = model.addWord('star');
assert.equal(pRet, pSaveList);
assert.deepEqual(Array.from(model._wordSet), ['star']);
});
test('word is trimmed correctly', function(){
var pRet = model.addWord(' star\t ');
assert.equal(pRet, pSaveList);
assert.deepEqual(Array.from(model._wordSet), ['star']);
});
test('existing word', function(){
var pRej = new MockPromise();
var stubReject = this.sinon.stub(Promise, 'reject').returns(pRej);
model.addWord('star');
var pRet = model.addWord('star');
assert.isTrue(stubReject.calledWith('existing'));
assert.equal(pRej, pRet);
});
});
suite('updateWord', function() {
var pSaveList;
setup(function() {
pSaveList = new MockPromise();
this.sinon.stub(model, '_saveDict').returns(pSaveList);
model._wordSet = new Set(['oldstar']);
});
test('normal case', function(){
var pRet = model.updateWord('oldstar', 'newstar');
assert.equal(pRet, pSaveList);
assert.deepEqual(Array.from(model._wordSet), ['newstar']);
});
test('old word === new word', function(){
var pRes = new MockPromise();
this.sinon.stub(Promise, 'resolve').returns(pRes);
var pRet = model.updateWord('oldstar', 'oldstar');
assert.isTrue(Promise.resolve.calledWith(['oldstar']),
'resolve() should be called with wordlist as parameter');
assert.equal(pRet, pRes);
});
test('new word is trimmed correctly', function(){
var pRet = model.updateWord('oldstar', ' newstar\t ');
assert.equal(pRet, pSaveList);
assert.deepEqual(Array.from(model._wordSet), ['newstar']);
});
test('existing new word', function(){
var pRej = new MockPromise();
var stubReject = this.sinon.stub(Promise, 'reject').returns(pRej);
model._wordSet.add('newstar');
var pRet = model.updateWord('oldstar', 'newstar');
assert.deepEqual(Array.from(model._wordSet), ['newstar']);
var pSaveListThen = pSaveList.mGetNextPromise();
assert.equal(pRet, pSaveListThen);
var pRetSaveListThen = pSaveList.mFulfillToValue(undefined);
assert.equal(pRetSaveListThen, pRej);
assert.isTrue(stubReject.calledWith('existing'));
});
});
test('removeWord', function() {
var pSaveList = new MockPromise();
this.sinon.stub(model, '_saveDict').returns(pSaveList);
model._wordSet = new Set(['star']);
var pRet = model.removeWord('star');
assert.equal(pRet, pSaveList);
assert.deepEqual(Array.from(model._wordSet), []);
});
suite('saveDict', function() {
// we test successive three calls to saveDict and make sure they're properly
// queued. We use native Promise to ease the checking since the chaining
// isn't very complex.
var rSaveQueue;
var pSave1, pSave2, pSave3;
var rDB1, rDB2, rDB3;
var pDB1, pDB2, pDB3;
setup(function() {
window.Promise.restore();
model._saveQueue = new Promise(resolve => (rSaveQueue = resolve));
pDB1 = new Promise(resolve => (rDB1 = resolve));
pDB2 = new Promise(resolve => (rDB2 = resolve));
pDB3 = new Promise(resolve => (rDB3 = resolve));
model._dbStore.setItems
.onFirstCall().returns(pDB1)
.onSecondCall().returns(pDB2)
.onThirdCall().returns(pDB3);
});
test('Correct queuing', function(done) {
model._wordSet = new Set(['a']);
stubWordListConverter.toBlob.returns('someblob');
pSave1 = model._saveDict();
pSave2 = model._saveDict();
pSave3 = model._saveDict();
assert.isFalse(model._dbStore.setItems.called);
assert.isTrue(window.WordListConverter.calledWith(['a']));
assert.isTrue(stubWordListConverter.toBlob.called);
// trigger the queue and resolve the first setItems Promise such that
// pSave1's then() will execute. Same for the second and third Promises.
rSaveQueue();
rDB1();
pSave1.then(() => {
assert.isTrue(model._dbStore.setItems.calledOnce);
rDB2();
}, done);
pSave2.then(() => {
assert.isTrue(model._dbStore.setItems.calledTwice);
rDB3();
}, done);
pSave3.then(() => {
assert.isTrue(model._dbStore.setItems.calledThrice);
assert.isTrue(model._dbStore.setItems.alwaysCalledWith({
wordlist: ['a'],
dictblob: 'someblob'
}));
done();
}, done);
});
test('Empty list => undefined blob', function(done) {
model._wordSet = new Set([]);
pSave1 = model._saveDict();
assert.isFalse(stubWordListConverter.toBlob.called);
rSaveQueue();
rDB1();
pSave1.then(() => {
assert.isTrue(model._dbStore.setItems.calledWith({
wordlist: [],
dictblob: undefined
}));
done();
}, done);
});
test('Word list is returned and sorted', function(done) {
model._wordSet = new Set(['apply', 'Banana', 'Apple']);
pSave1 = model._saveDict();
rSaveQueue();
rDB1();
pSave1.then(wordList => {
assert.deepEqual(wordList, ['Apple', 'apply', 'Banana']);
done();
}, done);
});
});
});

View file

@ -0,0 +1,883 @@
'use strict';
/* global WordListConverter */
require('/js/settings/word_list_converter.js');
suite('WordListConverter', function() {
var expected;
var actual;
// Our test strategy is very simple here: we feed a few word lists into the
// converter and see if the resulting blob is the same as the hard-coded
// "correct" blob.
//
// Indeed, there is possible further improvement for this test strategy:
// For example, in the character frequency table, the order of same-frequency
// characters should not matter. Within the same level of the TST, the order
// of nodes linked by next pointers should not matter either (assuming they
// have the same frequency).
//
// Ultimately, we might still need to take such ordering-neutral comparison
// into consideration when we test the blobs, some day, if the underlying JS
// implementation used by the converter changes and if such change modifies
// the ordering assumed in the "correct" blob.
// This test script starts with simple tests. Tests become more and more
// complex (with differnet unicode character sets and combinations) in later
// suites. If more than one tests fail, it is wise to fix simple tests first,
// such as latin, before looking to complex ones such as Hindi/Emoji.
// For binary structure of a dictionary blob, please see:
// https://wiki.mozilla.org/Gaia/System/Keyboard/IME/Latin/Dictionary
// The hardcoded blob may serve as extra examples for that wiki page.
var compareBlobs = function(actual, expected) {
for (var i = 0; i < actual.length; i++) {
assert.strictEqual(actual[i], expected[i], `position ${ i } differs`);
}
assert.equal(actual.length, expected.length, 'length of two blobs differs');
};
test('Blob type is ArrayBuffer', function(){
assert.instanceOf(new WordListConverter(['a']).toBlob(), ArrayBuffer);
});
suite('Basic latin characters', function() {
test('single word', function(){
actual = new Uint8Array(new WordListConverter(['apple']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54, // FxOSDICT
0x0, 0x0, 0x0, 0x1, // Unused bytes, version 1
0x5, // longest word is 5-char long
0x0, 0x4, // four characters in char table
0x0, 0x70, 0x0, 0x0, 0x0, 0x2, // p, global occurrence = 2
0x0, 0x61, 0x0, 0x0, 0x0, 0x1, // a, global occurrence = 1
0x0, 0x6c, 0x0, 0x0, 0x0, 0x1, // l, global occurrence = 1
0x0, 0x65, 0x0, 0x0, 0x0, 0x1, // e, global occurrence = 1
0x8a, 0x61, // TST tree follow...
0x8a, 0x70,
0x8a, 0x70,
0x8a, 0x6c,
0x8a, 0x65,
0x0a
]);
compareBlobs(actual, expected);
});
test('multiple words', function(){
actual = new Uint8Array(new WordListConverter(
['apple', 'orange', 'apply', 'organic', 'blueberry']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x9, // longest word is 9-char long
0x0, 0xd, // 13 characters in char table
0x0, 0x61, 0x0, 0x0, 0x0, 0x4,
0x0, 0x70, 0x0, 0x0, 0x0, 0x4,
0x0, 0x65, 0x0, 0x0, 0x0, 0x4,
0x0, 0x72, 0x0, 0x0, 0x0, 0x4,
0x0, 0x6c, 0x0, 0x0, 0x0, 0x3,
0x0, 0x6f, 0x0, 0x0, 0x0, 0x2,
0x0, 0x6e, 0x0, 0x0, 0x0, 0x2,
0x0, 0x67, 0x0, 0x0, 0x0, 0x2,
0x0, 0x79, 0x0, 0x0, 0x0, 0x2,
0x0, 0x62, 0x0, 0x0, 0x0, 0x2,
0x0, 0x69, 0x0, 0x0, 0x0, 0x1,
0x0, 0x63, 0x0, 0x0, 0x0, 0x1,
0x0, 0x75, 0x0, 0x0, 0x0, 0x1,
0xaa, 0x61, 0x0, 0x0, 0x2f, // root node 'a', with next pointer
0x8a, 0x70, // center node 'p'
0x8a, 0x70, // center node 'p'
0x8a, 0x6c, // center node 'l'
0xaa, 0x65, 0x0, 0x0, 0x11, // center node 'e', with next pointer
0x0a, // EndOfWord
0x8a, 0x79, // node 'y', next-pointed from 'e' above
0x0a, // EndOfWord
0x8a, 0x6f, // node 'o', next-pointed from root 'b'
0x8a, 0x72, // center node 'r'
0xaa, 0x61, 0x0, 0x0, 0x24, // center node 'a', with next pointer
0x8a, 0x6e, // center node 'n'
0x8a, 0x67, // center node 'g'
0x8a, 0x65, // center node 'e'
0x0a, // EndOfWord
0x8a, 0x67, // node 'g', next-pointed from 'a' above
0x8a, 0x61, // node 'a'
0x8a, 0x6e, // node 'n'
0x8a, 0x69, // node 'i'
0x8a, 0x63, // node 'c'
0x0a, // EndOfWord
0xaa, 0x62, 0x0, 0x0, 0x14, // node 'b', next-pointed by root 'a'
// and next-point to 'o' on 0x14
0x8a, 0x6c, // node 'l'
0x8a, 0x75, // node 'u'
0x8a, 0x65, // node 'e'
0x8a, 0x62, // node 'b'
0x8a, 0x65, // node 'e'
0x8a, 0x72, // node 'r'
0x8a, 0x72, // node 'r'
0x8a, 0x79, // node 'y'
0x0a // EndOfWord
]);
compareBlobs(actual, expected);
});
// Uppercase/lowercase/diacritics are treated as different characters.
test('captilization', function(){
actual = new Uint8Array(new WordListConverter(
['mozIlla', 'Mozilla', 'mozilla', 'MOZILLA']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x7,
0x0, 0xc,
0x0, 0x6c, 0x0, 0x0, 0x0, 0x6,
0x0, 0x6f, 0x0, 0x0, 0x0, 0x3,
0x0, 0x7a, 0x0, 0x0, 0x0, 0x3,
0x0, 0x61, 0x0, 0x0, 0x0, 0x3,
0x0, 0x6d, 0x0, 0x0, 0x0, 0x2,
0x0, 0x49, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4d, 0x0, 0x0, 0x0, 0x2,
0x0, 0x69, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4c, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4f, 0x0, 0x0, 0x0, 0x1,
0x0, 0x5a, 0x0, 0x0, 0x0, 0x1,
0x0, 0x41, 0x0, 0x0, 0x0, 0x1,
0xaa, 0x4d, 0x0, 0x0, 0x22, // 'M', next is 'm' at 0x22
0xaa, 0x4f, 0x0, 0x0, 0x15, // 'O', next is 'o' at 0x15
0x8a, 0x5a, // 'Z'
0x8a, 0x49, // 'I'
0x8a, 0x4c, // 'L'
0x8a, 0x4c, // 'L'
0x8a, 0x41, // 'A'
0x0a,
0x8a, 0x6f, // 'o'
0x8a, 0x7a, // 'z'
0x8a, 0x69, // 'i'
0x8a, 0x6c, // 'l'
0x8a, 0x6c, // 'l'
0x8a, 0x61, // 'a'
0x0a,
0x8a, 0x6d, // 'm'
0x8a, 0x6f, // 'o'
0x8a, 0x7a, // 'z'
0xaa, 0x49, 0x0, 0x0, 0x34, // 'i', next is 'I' at 0x34
0x8a, 0x6c, // 'l'
0x8a, 0x6c, // 'l'
0x8a, 0x61, // 'a'
0x0a,
0x8a, 0x69, // 'I'
0x8a, 0x6c, // 'l'
0x8a, 0x6c, // 'l'
0x8a, 0x61, // 'a'
0x0a
]);
compareBlobs(actual, expected);
});
test('diacritics', function(){
actual = new Uint8Array(new WordListConverter(
['mÖz', 'Ɱoz', 'Moz', 'mOz', 'ḿoƶ']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x3,
0x0, 0x9,
0x0, 0x7a, 0x0, 0x0, 0x0, 0x4,
0x0, 0x6f, 0x0, 0x0, 0x0, 0x3,
0x0, 0x6d, 0x0, 0x0, 0x0, 0x2,
0x0, 0xd6, 0x0, 0x0, 0x0, 0x1,
0x2c, 0x6e, 0x0, 0x0, 0x0, 0x1, // 'Ɱ' (\u2c6e)
0x0, 0x4d, 0x0, 0x0, 0x0, 0x1,
0x0, 0x4f, 0x0, 0x0, 0x0, 0x1,
0x1e, 0x3f, 0x0, 0x0, 0x0, 0x1, // 'ḿ' (\u1e3f)
0x1, 0xb6, 0x0, 0x0, 0x0, 0x1, // 'ƶ' (\u01b6)
0xaa, 0x4d, 0x0, 0x0, 0x16,
0x8a, 0x6f,
0x8a, 0x7a,
0x0a,
0xea, 0x1e, 0x3f, 0x0, 0x0, 0x28, // 2-byte char 'ḿ' with next pointer
0x8a, 0x6f,
0xca, 0x1, 0xb6, // 2-byte char 'ƶ' as center node
0x0a,
0xaa, 0x6d, 0x0, 0x0, 0xa,
0xaa, 0x4f, 0x0, 0x0, 0x23,
0x8a, 0x7a,
0x0a,
0x8a, 0xd6,
0x8a, 0x7a,
0x0a,
0xca, 0x2c, 0x6e, // 2-byte char 'Ɱ'
0x8a, 0x6f,
0x8a, 0x7a,
0x0a
]);
compareBlobs(actual, expected);
});
});
// Although we don't really expect the presence of non-latin characters in the
// blob, we still need to check that the converter won't fail (either "soft"
// failure by encoding wrong data (that would affect latin prediction) or a
// "strong" failure by throwing an error) when fed with such characters.
// The following "correct" blobs are manually pre-tested to make sure they
// still produce latin-character predictions correctly.
// (Actually, I believe CJK predictions should also work, as long as we have a
// sensible |nearByKeys| structure, but... there is no such structure that
// can be (even remotely) sensible.)
suite('CJK characters', function() {
test('basic CJK test with only CJK chars', function(){
actual = new Uint8Array(new WordListConverter(
// The words are Hello / Good Morning / Good Evening in CJK.
['你好', '你好嗎', 'こんにちは', 'こんばんは', '안녕하세요']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x5, // longest word is 5-char long
0x0, 0xe, // 14 characters in char table
0x30, 0x93, 0x0, 0x0, 0x0, 0x3,
0x4f, 0x60, 0x0, 0x0, 0x0, 0x2,
0x59, 0x7d, 0x0, 0x0, 0x0, 0x2,
0x30, 0x53, 0x0, 0x0, 0x0, 0x2,
0x30, 0x6f, 0x0, 0x0, 0x0, 0x2,
0x55, 0xce, 0x0, 0x0, 0x0, 0x1,
0x30, 0x6b, 0x0, 0x0, 0x0, 0x1,
0x30, 0x61, 0x0, 0x0, 0x0, 0x1,
0x30, 0x70, 0x0, 0x0, 0x0, 0x1,
0xc5, 0x48, 0x0, 0x0, 0x0, 0x1,
0xb1, 0x55, 0x0, 0x0, 0x0, 0x1,
0xd5, 0x58, 0x0, 0x0, 0x0, 0x1,
0xc1, 0x38, 0x0, 0x0, 0x0, 0x1,
0xc6, 0x94, 0x0, 0x0, 0x0, 0x1,
0xea, 0x30, 0x53, 0x0, 0x0, 0x30, // 'こ', next-point to '你'
0xca, 0x30, 0x93, // 'ん'
0xea, 0x30, 0x6b, 0x0, 0x0, 0x16, // 'に', next-point to 'ば'
0xca, 0x30, 0x61, // 'ち'
0xca, 0x30, 0x6f, // 'は'
0x0a,
0xca, 0x30, 0x70, // 'ば'
0xca, 0x30, 0x93, // 'ん'
0xca, 0x30, 0x6f, // 'は'
0x0a,
0xca, 0xc5, 0x48, // '안'
0xca, 0xb1, 0x55, // '녕'
0xca, 0xd5, 0x58, // '하'
0xca, 0xc1, 0x38, // '세'
0xca, 0xc6, 0x94, // '요'
0x0a,
0xea, 0x4f, 0x60, 0x0, 0x0, 0x20, // '你', next-point to '안'
0xca, 0x59, 0x7d, // '好'
0x2a, 0x0, 0x0, 0x3d, // EndOfWord, next-point to '嗎'
0xca, 0x55, 0xce, // '嗎'
0x0a
]);
compareBlobs(actual, expected);
});
// the test is crafted that the composing bytes of the chinese characters
// (e.g. 0x4f = 'O', 0x59 = 'Y', 0x55 = 'U', 0xce = 'Î') are re-used, as
// single-byte latin chars, appearing in other words.
// again, the "correct" blob has been pre-tested to make sure that the
// prediction of those single-byte latin chars works.
test('CJK+latin chars', function(){
actual = new Uint8Array(new WordListConverter(
['SAY', 'SUB', '你好', 'LATÎN', 'LATIN', 'SON', 'RON',
'你好嗎', 'RUN', 'run', '好嗎', 'Uni', 'UnÎdentity']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0xa,
0x0, 0x17,
0x0, 0x4e, 0x0, 0x0, 0x0, 0x5,
0x0, 0x55, 0x0, 0x0, 0x0, 0x4,
0x0, 0x6e, 0x0, 0x0, 0x0, 0x4,
0x0, 0x53, 0x0, 0x0, 0x0, 0x3,
0x0, 0x41, 0x0, 0x0, 0x0, 0x3,
0x59, 0x7d, 0x0, 0x0, 0x0, 0x3,
0x4f, 0x60, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4c, 0x0, 0x0, 0x0, 0x2,
0x0, 0x54, 0x0, 0x0, 0x0, 0x2,
0x0, 0xce, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4f, 0x0, 0x0, 0x0, 0x2,
0x0, 0x52, 0x0, 0x0, 0x0, 0x2,
0x55, 0xce, 0x0, 0x0, 0x0, 0x2,
0x0, 0x69, 0x0, 0x0, 0x0, 0x2,
0x0, 0x74, 0x0, 0x0, 0x0, 0x2,
0x0, 0x59, 0x0, 0x0, 0x0, 0x1,
0x0, 0x42, 0x0, 0x0, 0x0, 0x1,
0x0, 0x49, 0x0, 0x0, 0x0, 0x1,
0x0, 0x72, 0x0, 0x0, 0x0, 0x1,
0x0, 0x75, 0x0, 0x0, 0x0, 0x1,
0x0, 0x64, 0x0, 0x0, 0x0, 0x1,
0x0, 0x65, 0x0, 0x0, 0x0, 0x1,
0x0, 0x79, 0x0, 0x0, 0x0, 0x1,
0xaa, 0x4c, 0x0, 0x0, 0x3a,
0x8a, 0x41,
0x8a, 0x54,
0xaa, 0x49, 0x0, 0x0, 0x11,
0x8a, 0x4e,
0x0a,
0x8a, 0xce,
0x8a, 0x4e,
0x0a,
0xaa, 0x72, 0x0, 0x0, 0x71,
0x8a, 0x75,
0x8a, 0x6e,
0x0a,
0xaa, 0x53, 0x0, 0x0, 0x4c,
0xaa, 0x41, 0x0, 0x0, 0x32,
0x8a, 0x59,
0x0a,
0x8a, 0x55,
0x8a, 0x42,
0x0a,
0xaa, 0x4f, 0x0, 0x0, 0x2d,
0x8a, 0x4e,
0x0a,
0xaa, 0x52, 0x0, 0x0, 0x20,
0xaa, 0x4f, 0x0, 0x0, 0x47,
0x8a, 0x4e,
0x0a,
0x8a, 0x55,
0x8a, 0x4e,
0x0a,
0xaa, 0x55, 0x0, 0x0, 0x16,
0x8a, 0x6e,
0xaa, 0x69, 0x0, 0x0, 0x59,
0x0a,
0x8a, 0xce,
0x8a, 0x64,
0x8a, 0x65,
0x8a, 0x6e,
0x8a, 0x74,
0x8a, 0x69,
0x8a, 0x74,
0x8a, 0x79,
0x0a,
0xca, 0x59, 0x7d,
0xca, 0x55, 0xce,
0x0a,
0xea, 0x4f, 0x60, 0x0, 0x0, 0x6a,
0xca, 0x59, 0x7d,
0x2a, 0x0, 0x0, 0x7e,
0xca, 0x55, 0xce,
0x0a
]);
compareBlobs(actual, expected);
});
});
// Our entire architecture has little (if any) regard to unicode combining
// characters such as Devanagari in Hindi, and treat each one unicode char as
// ...just one indepedent character. Still, we need to test if the converter
// maintains such treatment, and like how we're testing in CJK, whether those
// characters won't interfere with latin chars.
suite('Hindi', function() {
test('basic Hindi chars with only Hindi chars', function(){
// The words are translated from "Hi", "Good morning" and "Thank you".
actual = new Uint8Array(new WordListConverter(
['नमस्ते', 'सुप्रभात', 'शुक्रिया']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x8,
0x0, 0xf,
0x9, 0x4d, 0x0, 0x0, 0x0, 0x3,
0x9, 0x38, 0x0, 0x0, 0x0, 0x2,
0x9, 0x24, 0x0, 0x0, 0x0, 0x2,
0x9, 0x41, 0x0, 0x0, 0x0, 0x2,
0x9, 0x30, 0x0, 0x0, 0x0, 0x2,
0x9, 0x3e, 0x0, 0x0, 0x0, 0x2,
0x9, 0x28, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2e, 0x0, 0x0, 0x0, 0x1,
0x9, 0x47, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2a, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2d, 0x0, 0x0, 0x0, 0x1,
0x9, 0x36, 0x0, 0x0, 0x0, 0x1,
0x9, 0x15, 0x0, 0x0, 0x0, 0x1,
0x9, 0x3f, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2f, 0x0, 0x0, 0x0, 0x1,
0xea, 0x9, 0x28, 0x0, 0x0, 0x2f,
0xca, 0x9, 0x2e,
0xca, 0x9, 0x38,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x24,
0xca, 0x9, 0x47,
0x0a,
0xca, 0x9, 0x38,
0xca, 0x9, 0x41,
0xca, 0x9, 0x2a,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x30,
0xca, 0x9, 0x2d,
0xca, 0x9, 0x3e,
0xca, 0x9, 0x24,
0x0a,
0xea, 0x9, 0x36, 0x0, 0x0, 0x16,
0xca, 0x9, 0x41,
0xca, 0x9, 0x15,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x30,
0xca, 0x9, 0x3f,
0xca, 0x9, 0x2f,
0xca, 0x9, 0x3e,
0x0a
]);
compareBlobs(actual, expected);
});
// similar to CJK+latin test. we re-use 0x4d = 'M', 0x38 = '8', 0x41 = 'A',
// 0x47 = 'G', 0x3f = '?' here.
test('Hindi+latin chars', function(){
actual = new Uint8Array(new WordListConverter(
['Apple?', 'नमस्ते', 'MAGAZINE', 'Game',
'सुप्रभात', 'AM?', 'शुक्रिया']).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x8,
0x0, 0x1c,
0x0, 0x41, 0x0, 0x0, 0x0, 0x4,
0x9, 0x4d, 0x0, 0x0, 0x0, 0x3,
0x0, 0x70, 0x0, 0x0, 0x0, 0x2,
0x0, 0x65, 0x0, 0x0, 0x0, 0x2,
0x0, 0x3f, 0x0, 0x0, 0x0, 0x2,
0x9, 0x38, 0x0, 0x0, 0x0, 0x2,
0x9, 0x24, 0x0, 0x0, 0x0, 0x2,
0x0, 0x4d, 0x0, 0x0, 0x0, 0x2,
0x0, 0x47, 0x0, 0x0, 0x0, 0x2,
0x9, 0x41, 0x0, 0x0, 0x0, 0x2,
0x9, 0x30, 0x0, 0x0, 0x0, 0x2,
0x9, 0x3e, 0x0, 0x0, 0x0, 0x2,
0x0, 0x6c, 0x0, 0x0, 0x0, 0x1,
0x9, 0x28, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2e, 0x0, 0x0, 0x0, 0x1,
0x9, 0x47, 0x0, 0x0, 0x0, 0x1,
0x0, 0x5a, 0x0, 0x0, 0x0, 0x1,
0x0, 0x49, 0x0, 0x0, 0x0, 0x1,
0x0, 0x4e, 0x0, 0x0, 0x0, 0x1,
0x0, 0x45, 0x0, 0x0, 0x0, 0x1,
0x0, 0x61, 0x0, 0x0, 0x0, 0x1,
0x0, 0x6d, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2a, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2d, 0x0, 0x0, 0x0, 0x1,
0x9, 0x36, 0x0, 0x0, 0x0, 0x1,
0x9, 0x15, 0x0, 0x0, 0x0, 0x1,
0x9, 0x3f, 0x0, 0x0, 0x0, 0x1,
0x9, 0x2f, 0x0, 0x0, 0x0, 0x1,
0xaa, 0x41, 0x0, 0x0, 0x42,
0xaa, 0x4d, 0x0, 0x0, 0xd,
0x8a, 0x3f,
0x0a,
0x8a, 0x70,
0x8a, 0x70,
0x8a, 0x6c,
0x8a, 0x65,
0x8a, 0x3f,
0x0a,
0xea, 0x9, 0x28, 0x0, 0x0, 0x67,
0xca, 0x9, 0x2e,
0xca, 0x9, 0x38,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x24,
0xca, 0x9, 0x47,
0x0a,
0xaa, 0x4d, 0x0, 0x0, 0x18,
0x8a, 0x41,
0x8a, 0x47,
0x8a, 0x41,
0x8a, 0x5a,
0x8a, 0x49,
0x8a, 0x4e,
0x8a, 0x45,
0x0a,
0xaa, 0x47, 0x0, 0x0, 0x2e,
0x8a, 0x61,
0x8a, 0x6d,
0x8a, 0x65,
0x0a,
0xca, 0x9, 0x38,
0xca, 0x9, 0x41,
0xca, 0x9, 0x2a,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x30,
0xca, 0x9, 0x2d,
0xca, 0x9, 0x3e,
0xca, 0x9, 0x24,
0x0a,
0xea, 0x9, 0x36, 0x0, 0x0, 0x4e,
0xca, 0x9, 0x41,
0xca, 0x9, 0x15,
0xca, 0x9, 0x4d,
0xca, 0x9, 0x30,
0xca, 0x9, 0x3f,
0xca, 0x9, 0x2f,
0xca, 0x9, 0x3e,
0x0a
]);
compareBlobs(actual, expected);
});
});
// Emoji characters on the supplimentary plane allow us to make sure surrogate
// pairs don't break. Indeed they shouldn't -- the converter should just treat
// them as two unicode "chars".
// We're getting into no man's land here, more so than Hindi's combining
// chars -- The original xml2dict.py can't even properly encode unicode chars
// outside BMP range since it expects a character to be at most 2 bytes.
suite('Emoji', function() {
// To make the tests more tangible (to human readers) those emojis are
// separately defined as const with their UTF-8 surrogate pair
// representation commented.
const emoSmile = '😀'; // '\ud83d\ude00'
const emoCar = '🚘'; // '\ud83d\ude98'
const emoTennis = '🎾'; // '\ud83c\udfbe'
const emoChesnut = '🌰'; // '\ud83c\udf30'
test('basic Emoji', function(){
actual = new Uint8Array(new WordListConverter(
[emoSmile,
emoSmile + emoCar,
emoTennis + emoChesnut + emoCar]).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0x6,
0x0, 0x6,
0xd8, 0x3d, 0x0, 0x0, 0x0, 0x4,
0xde, 0x0, 0x0, 0x0, 0x0, 0x2,
0xde, 0x98, 0x0, 0x0, 0x0, 0x2,
0xd8, 0x3c, 0x0, 0x0, 0x0, 0x2,
0xdf, 0xbe, 0x0, 0x0, 0x0, 0x1,
0xdf, 0x30, 0x0, 0x0, 0x0, 0x1,
0xea, 0xd8, 0x3c, 0x0, 0x0, 0x16,
0xca, 0xdf, 0xbe,
0xca, 0xd8, 0x3c,
0xca, 0xdf, 0x30,
0xca, 0xd8, 0x3d,
0xca, 0xde, 0x98,
0x0a,
0xca, 0xd8, 0x3d,
0xca, 0xde, 0x0,
0x2a, 0x0, 0x0, 0x20,
0xca, 0xd8, 0x3d,
0xca, 0xde, 0x98,
0x0a
]);
compareBlobs(actual, expected);
});
// again we reuse some single byte from the emoji surrogate pairs in
// the CJK/latin characters here, to check whether the converter doesn't
// mess up.
// 0x3c = '<', 0x3d = '=', 0x30 = '0'
// \u5398 = '厘', \u58be = '墾'
// \u30de = 'マ', \u30df = 'ミ'
// \u30d8 = 'ヘ' (note: this is Katagana "he", not Hiragana)
// マスタークラス is "Master Class" in Japanese
// 墾丁 is a tourist spot in Taiwan
// 厘米 is a way to say centimeter in Chinese
// ミリメートル is millimeter in Japanese
test('Emoji + CJK and latin words', function(){
actual = new Uint8Array(new WordListConverter(
['<' + emoChesnut + '>',
'マスター' + '=' + emoCar + '=' + 'クラス',
emoTennis + '墾丁',
emoCar + '厘米' + 'ミリメートル' + emoChesnut,
emoSmile + 'ヘ' + emoCar,
emoSmile
]).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0xc,
0x0, 0x19,
0xd8, 0x3d, 0x0, 0x0, 0x0, 0x5,
0xd8, 0x3c, 0x0, 0x0, 0x0, 0x3,
0xde, 0x98, 0x0, 0x0, 0x0, 0x3,
0xdf, 0x30, 0x0, 0x0, 0x0, 0x2,
0x30, 0xb9, 0x0, 0x0, 0x0, 0x2,
0x30, 0xfc, 0x0, 0x0, 0x0, 0x2,
0x0, 0x3d, 0x0, 0x0, 0x0, 0x2,
0xde, 0x0, 0x0, 0x0, 0x0, 0x2,
0x0, 0x3c, 0x0, 0x0, 0x0, 0x1,
0x0, 0x3e, 0x0, 0x0, 0x0, 0x1,
0x30, 0xde, 0x0, 0x0, 0x0, 0x1,
0x30, 0xbf, 0x0, 0x0, 0x0, 0x1,
0x30, 0xaf, 0x0, 0x0, 0x0, 0x1,
0x30, 0xe9, 0x0, 0x0, 0x0, 0x1,
0xdf, 0xbe, 0x0, 0x0, 0x0, 0x1,
0x58, 0xbe, 0x0, 0x0, 0x0, 0x1,
0x4e, 0x1, 0x0, 0x0, 0x0, 0x1,
0x53, 0x98, 0x0, 0x0, 0x0, 0x1,
0x7c, 0x73, 0x0, 0x0, 0x0, 0x1,
0x30, 0xdf, 0x0, 0x0, 0x0, 0x1,
0x30, 0xea, 0x0, 0x0, 0x0, 0x1,
0x30, 0xe1, 0x0, 0x0, 0x0, 0x1,
0x30, 0xc8, 0x0, 0x0, 0x0, 0x1,
0x30, 0xeb, 0x0, 0x0, 0x0, 0x1,
0x30, 0xd8, 0x0, 0x0, 0x0, 0x1,
0xaa, 0x3c, 0x0, 0x0, 0x1e,
0xca, 0xd8, 0x3c,
0xca, 0xdf, 0x30,
0x8a, 0x3e,
0x0a,
0xea, 0xd8, 0x3c, 0x0, 0x0, 0x41,
0xca, 0xdf, 0xbe,
0xca, 0x58, 0xbe,
0xca, 0x4e, 0x1,
0x0a,
0xea, 0x30, 0xde, 0x0, 0x0, 0xe,
0xca, 0x30, 0xb9,
0xca, 0x30, 0xbf,
0xca, 0x30, 0xfc,
0x8a, 0x3d,
0xca, 0xd8, 0x3d,
0xca, 0xde, 0x98,
0x8a, 0x3d,
0xca, 0x30, 0xaf,
0xca, 0x30, 0xe9,
0xca, 0x30, 0xb9,
0x0a,
0xca, 0xd8, 0x3d,
0xea, 0xde, 0x0, 0x0, 0x0, 0x58,
0x2a, 0x0, 0x0, 0x4e,
0xca, 0x30, 0xd8,
0xca, 0xd8, 0x3d,
0xca, 0xde, 0x98,
0x0a,
0xca, 0xde, 0x98,
0xca, 0x53, 0x98,
0xca, 0x7c, 0x73,
0xca, 0x30, 0xdf,
0xca, 0x30, 0xea,
0xca, 0x30, 0xe1,
0xca, 0x30, 0xfc,
0xca, 0x30, 0xc8,
0xca, 0x30, 0xeb,
0xca, 0xd8, 0x3c,
0xca, 0xdf, 0x30,
0x0a
]);
compareBlobs(actual, expected);
});
});
suite('Word list with frequency information', function() {
suite('Rejections', function() {
var dummy;
test('Reject mixed input', function(){
assert.throws(() => {
dummy =
new WordListConverter(['simpleword', {w: 'anotherword', f: 0.5}]);
}, /^Type mismatch\. previous: \S+, this: \S+$/);
assert.throws(() => {
dummy =
new WordListConverter([{w: 'simpleword', f: 0.5}, 'anotherword']);
}, /^Type mismatch\. previous: \S+, this: \S+$/);
});
test('Reject object elements without proper fields', function(){
assert.throws(() => {
dummy = new WordListConverter([{f: 0.5}]);
}, /^"w" field not found in word$/);
assert.throws(() => {
dummy = new WordListConverter([{w: 'word'}]);
}, /^"f" field not found in word$/);
assert.throws(() => {
dummy = new WordListConverter([{w: 'word', f: 1}]);
}, /^"f" value not in allowed range$/);
assert.throws(() => {
dummy = new WordListConverter([{w: 'word', f: 100}]);
}, /^"f" value not in allowed range$/);
assert.throws(() => {
dummy = new WordListConverter([{w: 'word', f: 255}]);
}, /^"f" value not in allowed range$/);
});
});
test('multiple words including capitalization differences', function(){
actual = new Uint8Array(new WordListConverter(
[{w: 'apple', f: 0.5},
{w: 'origami', f: 0.18},
{w: 'orange', f: 0.6},
{w: 'apply', f: 0.8},
{w: 'organic', f: 0.7},
{w: 'blueberry', f: 0.15},
{w: 'asymptotic', f: 0.2},
{w: 'Asia', f: 0.75}
]).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0xa,
0x0, 0x11,
0x0, 0x61, 0x0, 0x0, 0x0, 0x7,
0x0, 0x70, 0x0, 0x0, 0x0, 0x5,
0x0, 0x72, 0x0, 0x0, 0x0, 0x5,
0x0, 0x69, 0x0, 0x0, 0x0, 0x5,
0x0, 0x65, 0x0, 0x0, 0x0, 0x4,
0x0, 0x6f, 0x0, 0x0, 0x0, 0x4,
0x0, 0x6c, 0x0, 0x0, 0x0, 0x3,
0x0, 0x67, 0x0, 0x0, 0x0, 0x3,
0x0, 0x79, 0x0, 0x0, 0x0, 0x3,
0x0, 0x6d, 0x0, 0x0, 0x0, 0x2,
0x0, 0x6e, 0x0, 0x0, 0x0, 0x2,
0x0, 0x63, 0x0, 0x0, 0x0, 0x2,
0x0, 0x62, 0x0, 0x0, 0x0, 0x2,
0x0, 0x73, 0x0, 0x0, 0x0, 0x2,
0x0, 0x74, 0x0, 0x0, 0x0, 0x2,
0x0, 0x75, 0x0, 0x0, 0x0, 0x1,
0x0, 0x41, 0x0, 0x0, 0x0, 0x1,
0xb9, 0x61, 0x0, 0x0, 0x2a, // root node 'a' as 'apply' is highest
// freq; 0xb9 = 0b10111001
// (c and n bits set, freq = 25),
// 25 is the frequency of 'apply', i.e.
// the most-frequent word from 'a'
0xb9, 0x70, 0x0, 0x0, 0x17, // second level 'p' as 'apply' is more
// frequent than 'asymptotic'
0x99, 0x70, // 0x99 = 0b10011001 (c bit set, f = 25)
0x99, 0x6c,
0xb9, 0x79, 0x0, 0x0, 0x14, // 'y' of apply, with next to 'e', as
// 'apply' is more frequent than 'apple'
0x19, // EoW of 'apply', freq = 25
0x90, 0x65, // 'e', freq = 16
0x10, // EoW of 'apple', freq = 16
0x87, 0x73, // 's', next-pointed from second-level
// 'p' below root-level 'a'
// freq = 7
0x87, 0x79,
0x87, 0x6d,
0x87, 0x70,
0x87, 0x74,
0x87, 0x6f,
0x87, 0x74,
0x87, 0x69,
0x87, 0x63,
0x7, // EoW of 'asymptotic'
0xb8, 0x41, 0x0, 0x0, 0x36, // root-level 'A' as next-pointed by
// root 'a', as 'Asia' is the second
// frequent (than 'apply') and more
// frequent than 'organic and
// 'blueberry'. freq = 24,
// 24 is the frequency of 'Asia', i.e.
// the most-frequent word from 'A'
0x98, 0x73,
0x98, 0x69,
0x98, 0x61,
0x18, // EoW of 'Asia'
0xb6, 0x6f, 0x0, 0x0, 0x62, // root-level 'o' as next-pointed by
// root 'A', as 'o' is the next frequent
// and freq = 22, frequency of 'organic'
0x96, 0x72, // 'r'
0xb6, 0x67, 0x0, 0x0, 0x4b, // 3rd-level 'g', next to 'a'. freq = 22
// = freq of 'organic'
0x96, 0x61,
0x96, 0x6e,
0x96, 0x69,
0x96, 0x63,
0x16, // EoW of 'organic'
0xb3, 0x61, 0x0, 0x0, 0x57, // 3rd-level 'a', next-pointed by
// 3rd-level 'g', below 'o' -> 'r';
// with a next pointer to 'i' as
// 'origami' freq is lower than 'orange'
// frequency is 19 = freq of 'orange'
0x93, 0x6e,
0x93, 0x67,
0x93, 0x65,
0x13, // EoW of 'orange', freq = 19
0x86, 0x69, // 3rd-level 'i', next-pointed by
// 3rd-level 'a', below 'o' -> 'r';
// freq is 6 = freq of 'origami'
0x86, 0x67,
0x86, 0x61,
0x86, 0x6d,
0x86, 0x69,
0x6, // EoW of 'origami'
0x85, 0x62, // root-level 'b' as nexted-pointed by
// root-level 'o'; no more next pointers
// freq = 5 i.e. freq of 'blueberry'
0x85, 0x6c,
0x85, 0x75,
0x85, 0x65,
0x85, 0x62,
0x85, 0x65,
0x85, 0x72,
0x85, 0x72,
0x85, 0x79,
0x5 // EoW of 'blueberry'
]);
compareBlobs(actual, expected);
});
test('word freqency = 0 is properly honored', function(){
actual = new Uint8Array(new WordListConverter(
[{w: 'apple', f: 0.5},
{w: 'toxicapple', f: 0}]).toBlob());
expected = new Uint8Array([
0x46, 0x78, 0x4f, 0x53, 0x44, 0x49, 0x43, 0x54,
0x0, 0x0, 0x0, 0x1,
0xa,
0x0, 0x9,
0x0, 0x70, 0x0, 0x0, 0x0, 0x4,
0x0, 0x61, 0x0, 0x0, 0x0, 0x2,
0x0, 0x6c, 0x0, 0x0, 0x0, 0x2,
0x0, 0x65, 0x0, 0x0, 0x0, 0x2,
0x0, 0x74, 0x0, 0x0, 0x0, 0x1,
0x0, 0x6f, 0x0, 0x0, 0x0, 0x1,
0x0, 0x78, 0x0, 0x0, 0x0, 0x1,
0x0, 0x69, 0x0, 0x0, 0x0, 0x1,
0x0, 0x63, 0x0, 0x0, 0x0, 0x1,
0xb0, 0x61, 0x0, 0x0, 0xe, // root 'a', next to 't', freq = 16
0x90, 0x70,
0x90, 0x70,
0x90, 0x6c,
0x90, 0x65,
0x10, // EoW of 'apple', freq = 16
0x80, 0x74, // root-next 't', freq = 0
0x80, 0x6f,
0x80, 0x78,
0x80, 0x69,
0x80, 0x63,
0x80, 0x61,
0x80, 0x70,
0x80, 0x70,
0x80, 0x6c,
0x80, 0x65,
0x0 // EoW of 'toxicapple', freq = 0
]);
compareBlobs(actual, expected);
});
});
});

View file

@ -0,0 +1,11 @@
'use strict';
// We need to isolate these global variables from multiple testing files by
// using "requireApp" in order to prevent the conflict.
if (!window.PAGE_INDEX_DEFAULT) {
window.PAGE_INDEX_DEFAULT = 0;
}
if (!window.InputMethods) {
window.InputMethods = {};
}

View file

@ -0,0 +1,289 @@
'use strict';
/* global PromiseStorage, MockIDBFactory, MockIDBDatabase */
require('/shared/test/unit/mocks/mock_event_target.js');
require('/test/unit/mock_indexeddb.js');
require('/js/shared/promise_storage.js');
suite('PromiseStorage', function() {
var name;
var storage;
var mockIndexedDB;
var stubIDB;
var p;
setup(function() {
name = 'storage';
mockIndexedDB = new MockIDBFactory();
this.sinon.spy(mockIndexedDB, 'open');
MockIDBFactory.attachToWindow(mockIndexedDB);
storage = new PromiseStorage(name);
p = storage.start();
});
suite('open database', function() {
test('error', function(done) {
var openReq = mockIndexedDB.open.firstCall.returnValue;
openReq.error = new Error('Mocked error');
openReq.readyState = 'done';
openReq.dispatchEvent({
type: 'error',
target: openReq
});
p.then(function() {
assert.isTrue(false, 'should not resolve');
}, function reject(err) {
assert.strictEqual(err.message, 'Mocked error');
})
.then(done, done);
});
test('upgradedneeded (0 -> 1)', function() {
stubIDB = new MockIDBDatabase({
name: storage.name,
verstion: 0
});
this.sinon.spy(stubIDB, 'createObjectStore');
var openReq = mockIndexedDB.open.firstCall.returnValue;
openReq.result = stubIDB;
openReq.readyState = 'done';
openReq.dispatchEvent({
type: 'upgradeneeded',
target: openReq,
oldVersion: 0,
newVersion: 1
});
assert.isTrue(stubIDB.createObjectStore.calledWith(storage.STORE_NAME));
});
test('success', function(done) {
stubIDB = new MockIDBDatabase({
name: storage.DB_NAME,
verstion: 1
});
var openReq = mockIndexedDB.open.firstCall.returnValue;
openReq.result = stubIDB;
openReq.readyState = 'done';
openReq.dispatchEvent({
type: 'success',
target: openReq
});
p.then(function() {
assert.isTrue(true, 'resolved');
}, function reject(err) {
assert.isTrue(false, 'should not reject');
})
.then(done, done);
});
suite('operations', function() {
var txn, store;
setup(function() {
var mOptions = {
name: storage.DB_NAME,
verstion: 1,
objectStoreNames: [storage.STORE_NAME]
};
stubIDB = new MockIDBDatabase(mOptions);
txn = stubIDB.transaction(storage.STORE_NAME);
store = txn.objectStore(storage.STORE_NAME);
this.sinon.spy(store, 'get');
this.sinon.spy(store, 'put');
this.sinon.spy(store, 'delete');
this.sinon.stub(stubIDB, 'transaction').returns(txn);
this.sinon.stub(txn, 'objectStore').returns(store);
var openReq = mockIndexedDB.open.firstCall.returnValue;
openReq.result = stubIDB;
openReq.readyState = 'done';
openReq.dispatchEvent({
type: 'success',
target: openReq
});
});
test('getItem', function(done) {
var data = {};
var pReturned = storage.getItem('foo');
p.then(function() {
assert.isTrue(store.get.calledWith('foo'));
var req = store.get.firstCall.returnValue;
req.readyState = 'done';
req.result = data;
req.dispatchEvent({
type: 'success',
target: req
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
return pReturned;
})
.then(function(d) {
assert.strictEqual(d, data, 'should resolve to data');
})
.then(done, done);
});
test('getItems', function(done) {
var data = [{}, {}];
var pReturned = storage.getItems(['foo', 'foo2']);
p.then(function() {
assert.isTrue(store.get.firstCall.calledWith('foo'));
assert.isTrue(store.get.getCall(1).calledWith('foo2'));
var req = store.get.firstCall.returnValue;
req.readyState = 'done';
req.result = data[0];
req.dispatchEvent({
type: 'success',
target: req
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
var req1 = store.get.getCall(1).returnValue;
req1.readyState = 'done';
req1.result = data[1];
req1.dispatchEvent({
type: 'success',
target: req1
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
return pReturned;
})
.then(function(d) {
assert.strictEqual(d[0], data[0], 'should resolve to data');
assert.strictEqual(d[1], data[1], 'should resolve to data');
})
.then(done, done);
});
test('setItem', function(done) {
var data = {};
var pReturned = storage.setItem('foo', data);
p.then(function() {
assert.isTrue(
stubIDB.transaction.calledWith(storage.STORE_NAME, 'readwrite'));
assert.isTrue(store.put.calledWith(data, 'foo'));
var req = store.put.firstCall.returnValue;
req.readyState = 'done';
req.result = 'foo';
req.dispatchEvent({
type: 'success',
target: req
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
return pReturned;
})
.then(function(k) {
assert.equal(k, 'foo');
})
.then(done, done);
});
test('setItems', function(done) {
var data = [{}, {}];
var pReturned = storage.setItems({
foo: data[0],
foo2: data[1]
});
p.then(function() {
assert.isTrue(
stubIDB.transaction.calledWith(storage.STORE_NAME, 'readwrite'));
assert.isTrue(store.put.firstCall.calledWith(data[0], 'foo'));
assert.isTrue(store.put.getCall(1).calledWith(data[1], 'foo2'));
var req = store.put.firstCall.returnValue;
req.readyState = 'done';
req.result = 'foo';
req.dispatchEvent({
type: 'success',
target: req
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
var req1 = store.put.getCall(1).returnValue;
req1.readyState = 'done';
req1.result = 'foo2';
req1.dispatchEvent({
type: 'success',
target: req1
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
return pReturned;
})
.then(function(k) {
assert.deepEqual(k, ['foo', 'foo2']);
})
.then(done, done);
});
test('deleteItem', function(done) {
var pReturned = storage.deleteItem('foo');
p.then(function() {
assert.isTrue(
stubIDB.transaction.calledWith(storage.STORE_NAME, 'readwrite'));
assert.isTrue(store.delete.calledWith('foo'));
var req = store.delete.firstCall.returnValue;
req.readyState = 'done';
req.result = undefined;
req.dispatchEvent({
type: 'success',
target: req
});
txn.dispatchEvent({
type: 'complete',
target: txn
});
return pReturned;
})
.then(function(k) {
assert.equal(k, undefined);
})
.then(done, done);
});
});
});
});

View file

@ -0,0 +1,252 @@
'use strict';
/* global AlternativesCharMenuView */
require('/js/views/alternatives_char_menu_view.js');
require('/js/views/key_view.js');
suite('Views > AlternativesCharMenuView', function() {
var menu = null;
var options = {
};
var KEY_WIDTH = 30;
var KEY_HEIGHT = 20;
var altChars = ['a', 'b', 'c'];
var fakeMenu;
var rootElement = document.createElement('div');
document.body.appendChild(rootElement);
var dummyKeyElement = document.createElement('button');
var viewManager = {
registerView: sinon.stub()
};
suite('basic show/hide testing', function() {
setup(function() {
menu = new AlternativesCharMenuView(rootElement, altChars, options,
viewManager);
});
test(' > show()', function() {
menu.show(dummyKeyElement);
assert.equal(rootElement.firstElementChild,
menu.getMenuContainer(),
'Menu was not inserted into root Element');
menu.hide();
});
test(' > hide()', function() {
menu.show(dummyKeyElement);
menu.hide();
assert.equal(rootElement.childElementCount, 0,
'Menu was not removed from root Element!');
});
});
suite(' > getMenuTarget() - single row case', function() {
setup(function() {
fakeMenu = document.createElement('div');
fakeMenu.style.position = 'absolute';
fakeMenu.style.fontSize = 0;
// insert 3 keys for 1 row
for (var i = 0; i < 3; i++) {
var child = document.createElement('div');
child.style.display = 'inline-block';
child.style.width = KEY_WIDTH + 'px';
child.style.height = KEY_HEIGHT + 'px';
fakeMenu.appendChild(child);
}
rootElement.appendChild(fakeMenu);
menu = new AlternativesCharMenuView(rootElement, altChars, options,
viewManager);
this.sinon.stub(menu, 'getMenuContainer');
menu.getMenuContainer.returns(fakeMenu);
this.sinon.stub(menu, 'getBoundingClientRect',
fakeMenu.getBoundingClientRect.bind(fakeMenu));
this.sinon.stub(menu, 'getLineHeight');
menu.getLineHeight.returns(KEY_HEIGHT);
});
teardown(function() {
menu.hide();
});
test('the first element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(rect.left, rect.top);
assert.equal(target, menu.altKeyTargets[0],
'Should map to the first element');
});
test('the second element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(fakeMenu.offsetLeft + KEY_WIDTH +
rect.left, rect.top);
assert.equal(target, menu.altKeyTargets[1],
'Should map to the second element');
});
test('the last element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(fakeMenu.offsetLeft +
fakeMenu.offsetWidth +
rect.left, rect.top);
assert.equal(target, menu.altKeyTargets[menu.altKeyTargets.length -1],
'Should map to the last element');
});
});
suite(' > getMenuTarget() - 2 row case', function() {
setup(function() {
fakeMenu = document.createElement('div');
fakeMenu.style.position = 'absolute';
fakeMenu.style.width = KEY_WIDTH * 3 + 'px'; // 2 row
// insert 6 keys for 2 rows
for (var i = 0; i < 6; i++) {
var child = document.createElement('div');
child.style.display = 'inline-block';
child.style.width = KEY_WIDTH + 'px';
child.style.height = KEY_HEIGHT + 'px';
fakeMenu.appendChild(child);
}
rootElement.appendChild(fakeMenu);
var altChars = ['a', 'b', 'c', 'd', 'e', 'f'];
menu = new AlternativesCharMenuView(rootElement, altChars, options,
viewManager);
this.sinon.stub(menu, 'getMenuContainer');
menu.getMenuContainer.returns(fakeMenu);
this.sinon.stub(menu, 'getBoundingClientRect',
fakeMenu.getBoundingClientRect.bind(fakeMenu));
this.sinon.stub(menu, 'getLineHeight');
menu.getLineHeight.returns(KEY_HEIGHT);
});
teardown(function() {
menu.hide();
});
test('2 row - the first element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(rect.left, fakeMenu.offsetTop +
fakeMenu.offsetHeight +
rect.top);
assert.equal(target, menu.altKeyTargets[0],
'Should map to the first element');
});
test('2 row - the second element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(fakeMenu.offsetLeft + KEY_WIDTH +
rect.left,
fakeMenu.offsetTop +
fakeMenu.offsetHeight +
rect.top);
assert.equal(target, menu.altKeyTargets[1],
'Should map to the second element');
});
test('2 row - the 4th element in the upper row', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(rect.left, rect.top);
assert.equal(target, menu.altKeyTargets[3],
'Should map to the 4th element');
});
});
suite(' > getMenuTarget() - 2 row with one empty cell case', function() {
setup(function() {
fakeMenu = document.createElement('div');
fakeMenu.style.position = 'absolute';
fakeMenu.style.width = KEY_WIDTH * 3 + 'px'; // 2 row
// insert 5 keys for 2 rows
// (would result in 3x2 cells with top top-right one being empty)
for (var i = 0; i < 5; i++) {
var child = document.createElement('div');
child.style.display = 'inline-block';
child.style.width = KEY_WIDTH + 'px';
child.style.height = KEY_HEIGHT + 'px';
fakeMenu.appendChild(child);
}
rootElement.appendChild(fakeMenu);
var altChars = ['a', 'b', 'c', 'd', 'e'];
menu = new AlternativesCharMenuView(rootElement, altChars, options,
viewManager);
this.sinon.stub(menu, 'getMenuContainer');
menu.getMenuContainer.returns(fakeMenu);
this.sinon.stub(menu, 'getBoundingClientRect',
fakeMenu.getBoundingClientRect.bind(fakeMenu));
this.sinon.stub(menu, 'getLineHeight');
menu.getLineHeight.returns(KEY_HEIGHT);
});
teardown(function() {
menu.hide();
});
test('2 row - the empty element', function() {
menu.show(dummyKeyElement);
var rect = fakeMenu.getBoundingClientRect();
var target = menu.getMenuTarget(fakeMenu.offsetLeft + rect.left +
KEY_WIDTH * 2, rect.top);
assert.equal(target, menu.altKeyTargets[4],
'Should map to the last element');
});
});
suite(' isMenuTarget()', function() {
test('isMenuTarget() returns true', function() {
assert.isTrue(menu.isMenuTarget(menu.altKeyTargets[0]));
});
test('isMenuTarget() returns false', function() {
assert.isFalse(menu.isMenuTarget({dummy: 'dummy'}));
});
});
});

View file

@ -0,0 +1,144 @@
'use strict';
/* global CandidatePanelView */
require('/js/views/base_view.js');
require('/js/views/candidate_panel_view.js');
function createFakeCandidates(count) {
var candidates = [];
for (var i = 0; i < count; i++) {
candidates.push('' + i);
}
return candidates;
}
suite('Views > CandidatePanelView', function() {
var candidatePanelView = null;
var viewManager = {
registerView: sinon.stub(),
getRemToPx: sinon.stub().returns(10)
};
suite('some basic functions', function() {
setup(function() {
var target = {};
var options = {className: 'basic-class'};
candidatePanelView = new CandidatePanelView(target, options, viewManager);
});
test('> render() ', function() {
assert.equal(candidatePanelView.element, null);
candidatePanelView.render();
assert.notEqual(candidatePanelView.element, null);
var classList = candidatePanelView.element.classList;
assert.isTrue(classList.contains('basic-class'),
'should contain the className specified in options');
});
test('> getHeight()', function() {
assert.equal(candidatePanelView.getHeight(), 32);
});
suite('> showCandidates()', function() {
test('> show 8 candidates', function() {
candidatePanelView.render();
candidatePanelView.showCandidates(createFakeCandidates(8));
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var row = suggestionsContainer.querySelector('.candidate-row-first');
assert.equal(row.childNodes.length, 8);
// Should not contain the toggle button
var toggleButton =
row.querySelector('.keyboard-candidate-panel-toggle-button');
assert.equal(toggleButton, null);
});
test('> show 9 candidates', function() {
candidatePanelView.render();
candidatePanelView.showCandidates(createFakeCandidates(9));
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var row = suggestionsContainer.querySelector('.candidate-row-first');
assert.equal(row.childNodes.length, 8);
// Should contain the toggle button
var toggleButton = candidatePanelView.element.
querySelector('.keyboard-candidate-panel-toggle-button');
assert.notEqual(toggleButton, null);
});
test('> candidates with different length', function() {
candidatePanelView.render();
var candidates = ['1', '2', '3' , '4', '55', '6', '7', '8'];
candidatePanelView.showCandidates(candidates);
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var row = suggestionsContainer.querySelector('.candidate-row-first');
assert.equal(row.childNodes.length, 7);
// Should contain the toggle button
var toggleButton = candidatePanelView.element.
querySelector('.keyboard-candidate-panel-toggle-button');
assert.notEqual(toggleButton, null);
candidatePanelView.showMoreCandidates(1, ['9']);
// Check it has the 2nd row
var rows = suggestionsContainer.querySelectorAll('.candidate-row');
assert.equal(rows.length, 2);
// Check 2nd row is shown as a grid
var candidateElements = rows[1].querySelectorAll('span');
assert.equal(candidateElements.length, 2);
assert.equal(candidateElements[0].textContent, '9');
assert.equal(candidateElements[0].dataset.data, '9');
assert.equal(candidateElements[0].style.flexGrow, 1);
// Will have a dummy element here make it look like a grid
assert.equal(candidateElements[1].style.flexGrow, 7);
});
});
suite('candidatePanel with even width for each candidate', function() {
setup(function() {
var options = { widthUnit: 1 };
candidatePanelView = new CandidatePanelView({}, options, viewManager);
});
test('Each candidate has the same width', function() {
candidatePanelView.render();
candidatePanelView.showCandidates(createFakeCandidates(5));
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var candidateElements = suggestionsContainer.
querySelectorAll('.candidate-row span');
assert.equal(candidateElements.length, 5);
for (var i = 0; i < candidateElements.length; i++) {
assert.equal(candidateElements[i].style.flexGrow, 1);
}
});
});
});
});

View file

@ -0,0 +1,60 @@
'use strict';
/* global EmojiKeyView */
require('/js/views/base_view.js');
require('/js/views/emoji_key_view.js');
suite('Views > KeyView', function() {
var emojiKeyView = null;
var viewManager = {
registerView: sinon.stub()
};
suite('some basic functions', function() {
setup(function() {
var target = {};
var options = {};
emojiKeyView = new EmojiKeyView(target, options, viewManager);
});
test(' > render()', function() {
assert.equal(emojiKeyView.element, null);
emojiKeyView.render();
assert.notEqual(emojiKeyView.element, null);
assert.isTrue(emojiKeyView.element.classList.contains('emoji'));
});
test('additional class name', function() {
var key = {
value: 'k',
};
var options = {
classNames: ['another-className']
};
var keyView = new EmojiKeyView(key, options, viewManager);
keyView.render();
options.classNames.forEach(function(className) {
assert.isTrue(keyView.element.classList.contains(className));
});
});
test('invoke viewManager.registerView', function() {
var mockViewManager = {
registerView: this.sinon.stub()
};
var target = {};
var keyView = new EmojiKeyView(target, {}, mockViewManager);
keyView.render();
assert.isTrue(mockViewManager.registerView.calledOnce);
assert.isTrue(mockViewManager.registerView.calledWith(target,
keyView));
});
});
});

View file

@ -0,0 +1,97 @@
'use strict';
/* global HandwritingPadView */
require('/js/views/handwriting_pad_view.js');
suite('Views > HandwritingPadView', function() {
var handwritingPadView;
var canvas;
var STROKE_WIDTH = 6;
var pressStart = {
moved: false,
clientX: 20,
clientY: 20
};
var pressMove = {
moved: true,
clientX: 25,
clientY: 25
};
var rootElement = document.createElement('div');
document.body.appendChild(rootElement);
document.body.style.pad = 0;
document.body.style.margin = 0;
var viewManager = {
registerView: sinon.stub()
};
suite('basic testing', function() {
setup(function() {
var target = {};
handwritingPadView = new HandwritingPadView(target, {}, viewManager);
handwritingPadView.render();
canvas = handwritingPadView.element;
});
test(' > get element', function() {
assert.equal(canvas instanceof HTMLCanvasElement, true);
});
test('invoke viewManager.registerView', function() {
var mockViewManager = {
registerView: this.sinon.stub()
};
var target = {};
var padView = new HandwritingPadView(target, {}, mockViewManager);
padView.render();
assert.isTrue(mockViewManager.registerView.calledOnce);
assert.isTrue(mockViewManager.registerView.calledWith(target,
padView));
});
test(' > drawHandwritingPad()', function() {
rootElement.appendChild(canvas);
var point = handwritingPadView.drawHandwritingPad(pressStart, true,
STROKE_WIDTH);
assert.equal(point.posX, pressStart.clientX * window.devicePixelRatio,
'Error occured in drawCanvas when press start!');
assert.equal(point.posY, pressStart.clientY * window.devicePixelRatio,
'Error occured in drawCanvas when press start!');
point = handwritingPadView.drawHandwritingPad(pressMove, false,
STROKE_WIDTH);
assert.equal(point.posX, pressMove.clientX * window.devicePixelRatio,
'Error occured in drawCanvas when press move!');
assert.equal(point.posY, pressMove.clientY * window.devicePixelRatio,
'Error occured in drawCanvas when press move!');
});
test(' > resize()', function() {
Object.defineProperty(window, 'devicePixelRatio', {
configurable: true,
get: () => 2 }
);
handwritingPadView.resize(200, 400);
var canvas = handwritingPadView.element;
assert.equal(canvas.width, '400');
assert.equal(canvas.height, '800');
assert.equal(canvas.style.width, '200px');
assert.equal(canvas.style.height, '400px');
});
});
});

View file

@ -0,0 +1,244 @@
'use strict';
/* global KeyView */
require('/js/views/key_view.js');
suite('Views > KeyView', function() {
var keyView = null;
var viewManager = {
registerView: sinon.stub()
};
suite('some basic functions', function() {
setup(function() {
var target = {};
var options = {};
keyView = new KeyView(target, options, viewManager);
});
test(' > render()', function() {
assert.equal(keyView.element, null);
keyView.render();
assert.notEqual(keyView.element, null);
});
test(' > highlight() with upper case', function() {
keyView.render();
assert.isFalse(keyView.element.classList.contains('highlighted'));
keyView.highlight({upperCase: true});
assert.isTrue(keyView.element.classList.contains('highlighted'));
assert.isTrue(keyView.element.classList.contains('uppercase-popup'));
});
test(' > highlight() with lower case', function() {
keyView.render();
assert.isFalse(keyView.element.classList.contains('highlighted'));
keyView.highlight({upperCase: false});
assert.isTrue(keyView.element.classList.contains('highlighted'));
assert.isTrue(keyView.element.classList.contains('lowercase-popup'));
});
test(' > unHighlight()', function() {
keyView.render();
keyView.highlight();
assert.isTrue(keyView.element.classList.contains('highlighted'));
keyView.unHighlight();
assert.isFalse(keyView.element.classList.contains('highlighted'));
assert.isFalse(keyView.element.classList.contains('uppercase-popup'));
assert.isFalse(keyView.element.classList.contains('lowercase-popup'));
});
});
suite('DOM structures tests', function() {
test('basic key', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k',
keyCode: 999,
keyCodeUpper: 9999,
targetPage: 2
};
var options = {
keyWidth: 99
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
assert.isTrue(keyView.element.classList.contains('keyboard-key'));
assert.isFalse(keyView.element.classList.contains('special-key'));
assert.equal(keyView.element.getAttribute('role'), 'key');
assert.equal(keyView.element.style.width, options.keyWidth + 'px');
// Keycode dataset
assert.equal(keyView.element.dataset.keycode, key.keyCode);
assert.equal(keyView.element.dataset.keycodeUpper, key.keyCodeUpper);
// target page dataset
assert.equal(keyView.element.dataset.targetPage, key.targetPage);
//visual wrapper element
var visualWrapper = keyView.element.firstElementChild;
assert.equal(visualWrapper.className, 'visual-wrapper');
// Inner label
var labelNode = visualWrapper.querySelector('.key-element');
assert.equal(labelNode.innerHTML, key.value);
assert.equal(labelNode.dataset.label, key.value);
// popups
var uppercasePopup = visualWrapper.querySelector('.uppercase.popup');
assert.equal(uppercasePopup.innerHTML, key.value);
var lowercasePopup = visualWrapper.querySelector('.lowercase.popup');
assert.equal(lowercasePopup.innerHTML, key.lowercaseValue);
});
test('special key', function() {
var key = {
isSpecialKey: true,
value: 'k',
lowercaseValue: 'l_k'
};
var options = {
keyWidth: 99
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
assert.isTrue(keyView.element.classList.contains('keyboard-key'));
assert.isTrue(keyView.element.classList.contains('special-key'));
});
test('additional attribute list', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k'
};
var options = {
keyWidth: 99,
attributeList: [
{key: 'testKey', value: 'testValue'},
{key: 'testKey2', value: 'testValue2'}
]
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
options.attributeList.forEach(function(attribute) {
assert.equal(keyView.element.getAttribute(attribute.key),
attribute.value);
});
});
test('additional class name', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k',
className: 'test-className'
};
var options = {
keyWidth: 99,
classNames: ['another-className']
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
assert.isTrue(keyView.element.classList.contains('keyboard-key'));
assert.isTrue(keyView.element.classList.contains(key.className));
options.classNames.forEach(function(className) {
assert.isTrue(keyView.element.classList.contains(className));
});
});
test('disabled key', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k',
disabled: true
};
var options = {
keyWidth: 99,
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
assert.equal(keyView.element.getAttribute('disabled'), 'true');
});
test('alt label node for option w/ altOutputChar case', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k',
};
var options = {
keyWidth: 99,
altOutputChar: '*k'
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
var visualWrapper = keyView.element.firstElementChild;
// Inner label
var labelNode = visualWrapper.querySelector('.key-element.lowercase');
assert.equal(labelNode.innerHTML, options.altOutputChar);
});
test('altNote', function() {
var key = {
value: 'k',
lowercaseValue: 'l_k',
longPressValue: '#k'
};
var options = {
keyWidth: 99,
altOutputChar: '*k'
};
var keyView = new KeyView(key, options, viewManager);
keyView.render();
var visualWrapper = keyView.element.firstElementChild;
var altNote = visualWrapper.querySelector('.alt-note');
assert.equal(altNote.innerHTML, key.longPressValue);
});
});
suite('Register View to viewManager', function() {
test('invoke viewManager.registerView', function() {
var mockViewManager = {
registerView: this.sinon.stub()
};
var target = {};
var keyView = new KeyView(target, {}, mockViewManager);
keyView.render();
assert.isTrue(mockViewManager.registerView.calledOnce);
assert.isTrue(mockViewManager.registerView.calledWith(target,
keyView));
});
});
});

View file

@ -0,0 +1,144 @@
'use strict';
/* global LatinCandidatePanelView, ViewUtils */
require('/js/views/base_view.js');
require('/js/views/view_utils.js');
require('/js/views/candidate_panel_view.js');
require('/js/views/latin_candidate_panel_view.js');
function createFakeCandidates(count) {
var candidates = [];
for (var i = 0; i < count; i++) {
candidates.push('' + i);
}
return candidates;
}
suite('Views > LatinCandidatePanelView', function() {
var candidatePanelView = null;
var viewManager = {
registerView: sinon.stub()
};
suite('some basic functions', function() {
setup(function() {
var target = {};
var options = {className: 'test-class'};
candidatePanelView =
new LatinCandidatePanelView(target, options, viewManager);
});
test('> render() ', function() {
assert.equal(candidatePanelView.element, null);
candidatePanelView.render();
assert.notEqual(candidatePanelView.element, null);
var classList = candidatePanelView.element.classList;
assert.isTrue(classList.contains('test-class'),
'should contain the className specified in options');
});
test('> show no candidates', function() {
candidatePanelView.render();
candidatePanelView.showCandidates([]);
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
// Should show dismiss button
var dismissButton =
candidatePanelView.element.querySelector('.dismiss-suggestions-button');
assert.notEqual(dismissButton, null);
assert.isTrue(dismissButton.classList.contains('hide'));
});
test('> show 3 candidates', function() {
candidatePanelView.render();
candidatePanelView.showCandidates(createFakeCandidates(3));
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var candidateElements = suggestionsContainer.querySelectorAll('span');
assert.equal(candidateElements.length, 3);
for (var i = 0; i < candidateElements.length; i++) {
assert.equal(candidateElements[i].textContent, i);
assert.equal(candidateElements[i].dataset.data, i);
}
// Should show dismiss button
var dismissButton =
candidatePanelView.element.querySelector('.dismiss-suggestions-button');
assert.notEqual(dismissButton, null);
assert.isFalse(dismissButton.classList.contains('hide'));
});
test('> show 3 candidates, with the first as auto correction', function() {
candidatePanelView.render();
candidatePanelView.showCandidates(['*1', '2', '3']);
var suggestionsContainer =
candidatePanelView.element.querySelector('.suggestions-container');
assert.notEqual(suggestionsContainer, null);
var candidateElements = suggestionsContainer.querySelectorAll('div');
assert.equal(candidateElements.length, 3);
// The first candidate should be highlighted
assert.isTrue(candidateElements[0].classList.contains('autocorrect'));
});
test('Candidates scaling to 0.6', function() {
candidatePanelView.render();
sinon.stub(ViewUtils, 'getScale', function() {
return 0.6;
});
var can = ['thisisverylongword', 'alsoverylongword', 'whatup'];
candidatePanelView.showCandidates(can);
var spans = candidatePanelView.element.querySelectorAll('span');
assert.equal(spans[0].textContent, can[0]);
assert.equal(spans[1].textContent, can[1]);
assert.equal(spans[2].textContent, can[2]);
assert.equal(spans[0].style.width, '166.667%');
assert.equal(spans[0].style.transformOrigin, 'left center 0px');
assert.equal(spans[0].style.transform, 'scale(0.6)');
ViewUtils.getScale.restore();
});
test('Candidats scaling to 0.5', function() {
candidatePanelView.render();
sinon.stub(ViewUtils, 'getScale', function() {
return 0.5;
});
var can = ['thisisverylongword', 'alsoverylongword', 'whatup'];
candidatePanelView.showCandidates(can);
var spans = candidatePanelView.element.querySelectorAll('span');
assert.equal(spans[0].textContent, 't…d');
assert.equal(spans[1].textContent, 'a…d');
assert.equal(spans[2].textContent, 'w…p');
assert.equal(spans[0].style.width, '200%');
assert.equal(spans[0].style.transformOrigin, 'left center 0px');
assert.equal(spans[0].style.transform, 'scale(0.5)');
ViewUtils.getScale.restore();
});
});
});

View file

@ -0,0 +1,523 @@
'use strict';
/* global LayoutPageView, KeyboardEvent */
require('/js/views/key_view.js');
require('/js/views/handwriting_pad_view.js');
require('/js/views/layout_page_view.js');
suite('Views > LayoutPageView', function() {
var dummyLayout = {
width: 2,
keys: [
[{ value: 'a' }, { value: 'b' }]
]
};
var viewManager = {
registerView: sinon.stub()
};
test(' > show() ', function() {
var pageView = new LayoutPageView(dummyLayout, {}, viewManager);
pageView.render();
pageView.show();
var container = pageView.element;
assert.equal(container.dataset.active, 'true');
});
test(' > hide() ', function() {
var pageView = new LayoutPageView(dummyLayout, {}, viewManager);
pageView.render();
pageView.hide();
var container = pageView.element;
assert.equal(container.dataset.active, null);
});
test(' > destroy() ', function() {
var pageView = new LayoutPageView(dummyLayout, {}, viewManager);
pageView.render();
pageView.destroy();
var container = pageView.element;
assert.equal(container.parentNode, null);
});
suite(' > render()', function() {
test('Should add last-row class to last row', function() {
var layout = {
width: 2,
keys: [
[{ value: 'a' }, { value: 'b' }],
[{ value: 'c' }, { value: 'd' }],
[{ value: 'e' }, { value: 'f' }]
]
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
var rows = container.querySelectorAll('.keyboard-row');
assert.equal(rows.length, 3);
assert.equal(rows[2].classList.contains('keyboard-last-row'), true);
var keys = container.querySelectorAll('.keyboard-key');
assert.equal(keys.length, 6);
});
test('layout.keyClassName should be added to all keys', function() {
var layout = {
width: 2,
keys: [
[{ value: 'a' }, { value: 'b' }]
],
keyClassName: 'c9'
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
assert.equal(container.querySelectorAll('.keyboard-key').length, 2);
assert.equal(container.querySelectorAll('.keyboard-key.c9').length, 2);
});
test('Keycode should be set', function() {
var layout = {
width: 9,
keys: [
[{ value: 'a', keyCode: 3 }, { value: 'b', keyCode: 98 }]
]
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
var keys = container.querySelectorAll('.keyboard-key');
assert.equal(keys[0].dataset.keycode, 3);
assert.equal(keys[1].dataset.keycode, 98);
});
test('On uppercase flag, uppercase on keys, ' +
'no lowercase class on container', function() {
var layout = {
width: 2,
keys: [
[{ value: 'a', uppercaseValue: 'A' },
{ value: 'b', uppercaseValue: 'B' }]
]
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
pageView.setUpperCaseLock({
isUpperCase: true,
isUpperCaseLocked: false
});
var container = pageView.element;
var keys = container.querySelectorAll('.keyboard-key .key-element');
assert.equal(keys.length, 4);
assert.equal(keys[0].textContent, 'A');
assert.isTrue(keys[1].classList.contains('lowercase'));
assert.equal(keys[1].textContent, 'a');
assert.equal(keys[2].textContent, 'B');
assert.isTrue(keys[3].classList.contains('lowercase'));
assert.equal(keys[3].textContent, 'b');
assert.isFalse(container.classList.contains('lowercase'));
});
test('No uppercase flag, uppercase on keys, ' +
'lowercase class on container', function() {
var layout = {
width: 2,
keys: [
[{ value: 'a', uppercaseValue: 'A' },
{ value: 'b', uppercaseValue: 'B' }]
]
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
pageView.setUpperCaseLock({
isUpperCase: false,
isUpperCaseLocked: false
});
var container = pageView.element;
var keys = container.querySelectorAll('.keyboard-key .key-element');
assert.equal(keys.length, 4);
assert.equal(keys[0].textContent, 'A');
assert.isTrue(keys[1].classList.contains('lowercase'));
assert.equal(keys[1].textContent, 'a');
assert.equal(keys[2].textContent, 'B');
assert.isTrue(keys[3].classList.contains('lowercase'));
assert.equal(keys[3].textContent, 'b');
assert.isTrue(container.classList.contains('lowercase'));
});
test('create keyboard with handwriting pad', function() {
var layout = {
handwritingPadOptions: {
ratio: 8.5,
rowspan: 3
},
keys: [
[
{ value: 'a', ratio: 1.5 }
], [
{ value: 'b', ratio: 1.5 }
], [
{ value: 'c', ratio: 1.5 }
], [
{ value: 'd', ratio: 10 }
]
]
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
var pads = container.querySelectorAll('.handwriting-pad');
assert.equal(pads.length, 1);
});
suite('CSS classes on pageView', function() {
test('with specificCssRule', function() {
var layout = {
width: 1,
keys: [],
layoutName: 'ar',
specificCssRule: true
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
assert.equal(container.classList.contains('ar'), true);
});
test('without specificCssRule', function() {
var layout = {
width: 1,
keys: [],
layoutName: 'ar',
specificCssRule: false
};
var pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
assert.equal(container.classList.contains('ar'), false);
});
});
});
suite(' > setUpperCaseLock()', function() {
var pageView = null;
setup(function() {
var layout = {
width: 2,
keys: [
[{ value: 'a' },
{ value: 'Shift', keyCode: KeyboardEvent.DOM_VK_CAPS_LOCK }
]
]
};
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
});
test('turn to uppercase', function() {
pageView.setUpperCaseLock({
isUpperCase: true,
isUpperCaseLocked: false
});
var container = pageView.element;
assert.isFalse(container.classList.contains('lowercase'));
var capsLockKey = container.querySelector(
'[data-keycode="' + KeyboardEvent.DOM_VK_CAPS_LOCK + '"]'
);
assert.isTrue(capsLockKey.classList.contains('kbr-key-active'));
assert.isFalse(capsLockKey.classList.contains('kbr-key-hold'));
assert.equal(capsLockKey.getAttribute('aria-pressed'), 'true');
});
test('turn to lowercase', function() {
pageView.setUpperCaseLock({
isUpperCase: false,
isUpperCaseLocked: false
});
var container = pageView.element;
assert.isTrue(container.classList.contains('lowercase'));
var capsLockKey = container.querySelector(
'[data-keycode="' + KeyboardEvent.DOM_VK_CAPS_LOCK + '"]'
);
assert.isFalse(capsLockKey.classList.contains('kbr-key-active'));
assert.isFalse(capsLockKey.classList.contains('kbr-key-hold'));
assert.equal(capsLockKey.getAttribute('aria-pressed'), 'false');
});
test('enable CapsLock', function() {
pageView.setUpperCaseLock({
isUpperCase: true,
isUpperCaseLocked: true
});
var container = pageView.element;
assert.isFalse(container.classList.contains('lowercase'));
var capsLockKey = container.querySelector(
'[data-keycode="' + KeyboardEvent.DOM_VK_CAPS_LOCK + '"]'
);
assert.isFalse(capsLockKey.classList.contains('kbr-key-active'));
assert.isTrue(capsLockKey.classList.contains('kbr-key-hold'));
assert.equal(capsLockKey.getAttribute('aria-pressed'), 'true');
});
});
suite(' > highlightKey() and unHighlightKey()', function() {
var pageView = null;
var keyView;
setup(function() {
var layout = {
width: 2,
keys: []
};
keyView = {
highlight: sinon.stub(),
unHighlight: sinon.stub()
};
var viewManager = {
getView: function() {
return keyView;
}
};
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
});
test('highlightKey() with upperCase', function() {
pageView.isUpperCase = true;
pageView.highlightKey({});
assert.isTrue(keyView.highlight.calledOnce);
assert.isTrue(keyView.highlight.calledWith({upperCase: true}));
});
test('highlightKey() with upperCase', function() {
pageView.isUpperCase = false;
pageView.highlightKey({});
assert.isTrue(keyView.highlight.calledOnce);
assert.isTrue(keyView.highlight.calledWith({upperCase: false}));
});
test('unHighlightKey()', function() {
pageView.unHighlightKey({});
assert.isTrue(keyView.unHighlight.calledOnce);
});
});
suite('> getHeight()', function() {
var layout;
var pageView = null;
var viewManager = {
registerView: sinon.stub(),
getRemToPx: sinon.stub(),
screenInPortraitMode: sinon.stub()
};
var rootElement;
setup(function() {
rootElement = document.createElement('div');
document.body.appendChild(rootElement);
layout = {
width: 2,
keys: [
[{ value: 'a' }, { value: 'b' }],
[{ value: 'c' }, { value: 'd' }],
[{ value: 'e' }, { value: 'f' }],
[{ value: 'g' }, { value: 'h' }]
]
};
});
teardown(function() {
document.body.removeChild(rootElement);
});
test('w/ 4 rows, portrait', function() {
viewManager.getRemToPx.returns(10);
viewManager.screenInPortraitMode.returns(true);
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
assert.equal(pageView.getHeight(), 4 * ((4.5 + 0.8) * 10));
});
test('w/ 4 rows, landscape', function() {
viewManager.getRemToPx.returns(10);
viewManager.screenInPortraitMode.returns(false);
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
assert.equal(pageView.getHeight(), 4 * ((3.8 + 0.8) * 10));
});
test('w/ 5 rows, portrait', function() {
layout.keys.push([{ value: 'i' }, { value: 'j' }]);
viewManager.getRemToPx.returns(10);
viewManager.screenInPortraitMode.returns(true);
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
assert.equal(pageView.getHeight(), 5 * ((3.4 + 0.8) * 10));
});
test('w/ 5 rows, landscape', function() {
layout.keys.push([{ value: 'i' }, { value: 'j' }]);
viewManager.getRemToPx.returns(10);
viewManager.screenInPortraitMode.returns(false);
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
assert.equal(pageView.getHeight(), 5 * ((2.85 + 0.8) * 10));
});
});
suite(' > getVisualData()', function() {
var layout = {
width: 2,
keys: [
[{ value: 'a' }, { value: 'b' }],
[{ value: 'c' }, { value: 'd' }],
[{ value: 'e' }, { value: 'f' }]
]
};
var pageView = null;
var viewManager = {
registerView: sinon.stub()
};
var rootElement;
setup(function() {
rootElement = document.createElement('div');
document.body.appendChild(rootElement);
});
teardown(function() {
document.body.removeChild(rootElement);
});
test('check getVisualData sanity', function() {
var layout = {
width: 10,
keys: [
[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
]
};
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
rootElement.appendChild(pageView.element);
var vData = pageView.getVisualData();
var visuals = [].slice.call(
document.querySelectorAll('.visual-wrapper'));
visuals.forEach(function(v, ix) {
assert.equal(v.offsetLeft, vData[ix].x, 'x for ' + ix);
assert.equal(v.offsetTop, vData[ix].y, 'y for ' + ix);
assert.equal(v.clientWidth, vData[ix].width, 'width for ' + ix);
assert.equal(v.clientHeight, vData[ix].height, 'height for ' + ix);
});
});
test('check getVisualData sanity for filled up space', function() {
var layout = {
width: 4,
keys: [
[{}, {}, {}]
]
};
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
rootElement.appendChild(pageView.element);
var vData = pageView.getVisualData();
var visuals = [].slice.call(
document.querySelectorAll('.visual-wrapper'));
visuals.forEach(function(v, ix) {
assert.equal(v.offsetLeft, vData[ix].x, 'x for ' + ix);
assert.equal(v.offsetTop, vData[ix].y, 'y for ' + ix);
assert.equal(v.clientWidth, vData[ix].width, 'width for ' + ix);
assert.equal(v.clientHeight, vData[ix].height, 'height for ' + ix);
});
});
test('getVisualData will return cached data', function() {
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var vData = pageView.getVisualData();
var vData2 = pageView.getVisualData();
assert.equal(vData, vData2);
});
test('getVisualData() will return cached data for different ' +
'size', function() {
pageView = new LayoutPageView(layout, {}, viewManager);
pageView.render();
var vData = pageView.getVisualData();
pageView.resize(480);
var vData2 = pageView.getVisualData();
assert.notEqual(vData, vData2);
var vData3 = pageView.getVisualData();
assert.equal(vData2, vData3);
});
});
});

View file

@ -0,0 +1,84 @@
'use strict';
/* global SwipeablePageView */
require('/js/views/key_view.js');
require('/js/views/layout_page_view.js');
require('/js/views/swipeable_section_view.js');
require('/js/views/swiping_detector.js');
require('/js/views/swipeable_panel_view.js');
require('/js/views/swipeable_page_view.js');
suite('Views > SwipeablePageView', function() {
var pageView = null;
var viewManager = {
registerView: sinon.stub()
};
setup(function() {
var dummyLayout = {
width: 2,
panelKeys: [
[{ value: 'a' }, { value: 'b' }]
],
keys: [
[{ value: 'a' }, { value: 'b' }]
]
};
pageView = new SwipeablePageView(dummyLayout, {},
viewManager);
});
suite('some basic functions', function() {
test('> render() ', function() {
assert.equal(pageView.element, null);
pageView.render();
assert.notEqual(pageView.element, null);
pageView.show();
assert.equal(pageView.element.dataset.active, 'true');
assert.ok(pageView.element.classList.contains('uppercase-only'));
var rows = pageView.element.querySelectorAll('.keyboard-row');
assert.equal(rows.length, 1);
assert.ok(rows[0].classList.contains('swipe-switching-buttons'));
});
});
suite('CSS classes on pageView', function() {
test('with specificCssRule', function() {
var layout = {
panelKeys: [],
keys: [],
layoutName: 'ar',
specificCssRule: true
};
var pageView = new SwipeablePageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
assert.equal(container.classList.contains('ar'), true);
});
test('without specificCssRule', function() {
var layout = {
width: 1,
panelKeys: [],
keys: [],
layoutName: 'ar',
specificCssRule: false
};
var pageView = new SwipeablePageView(layout, {}, viewManager);
pageView.render();
var container = pageView.element;
assert.equal(container.classList.contains('ar'), false);
});
});
});

View file

@ -0,0 +1,395 @@
'use strict';
/* global SwipeablePanelView */
require('/js/views/base_view.js');
require('/js/views/key_view.js');
require('/js/views/emoji_key_view.js');
require('/js/views/swiping_detector.js');
require('/js/views/swipeable_panel_view.js');
suite('Views > SwipeablePanelView', function() {
var panelView = null;
var viewManager = {
registerView: sinon.stub()
};
var layout = {
panelKeys: []
};
var sectionCount = 4;
for (var i = 0; i < 18 * sectionCount; i++) {
layout.panelKeys.push({ value: i, type: 'emoji' });
}
setup(function() {
var options = {
};
panelView = new SwipeablePanelView(layout, options,
viewManager);
});
suite('some basic functions', function() {
test('> render() ', function() {
assert.equal(panelView.element, null);
panelView.render();
assert.notEqual(panelView.element, null);
});
test('> render() check indicator elements', function() {
panelView.render();
var indicators = panelView.element.querySelectorAll('.section-indicator');
assert.equal(indicators.length, sectionCount);
assert.ok(indicators[0].classList.contains('active'));
});
test('> move to the next section', function() {
panelView.render();
panelView.gotoSection(1);
var indicators = panelView.element.querySelectorAll('.section-indicator');
assert.isFalse(indicators[0].classList.contains('active'));
assert.ok(indicators[1].classList.contains('active'));
});
});
suite('utilize different KeyView type', function() {
test('> render() with KeyView ', function() {
var layout = {
panelKeys: [{value: 'key1'}, {value: 'key2'}]
};
var panelView = new SwipeablePanelView(layout, {}, viewManager);
panelView.render();
var keyView = panelView.element.querySelector('button.keyboard-key');
assert.notEqual(keyView, null);
});
test('> render() with EmojiKeyView ', function() {
var layout = {
panelKeys: [{value: 'key1', type: 'emoji'},
{value: 'key2', type: 'emoji'}]
};
var panelView = new SwipeablePanelView(layout, {}, viewManager);
panelView.render();
var keyView = panelView.element.querySelector('button.emoji');
assert.notEqual(keyView, null);
});
});
suite('Render different number of elements', function() {
test('> render() with no elements', function() {
var layout = {
panelKeys: [{value: 'key1'}, {value: 'key2'}]
};
panelView = new SwipeablePanelView(layout, {}, viewManager);
panelView.render();
assert.equal(panelView.sections[1], null);
});
test('> render() with 2 emoji elements', function() {
var layout = {
panelKeys: [{value: 'key1', type: 'emoji'},
{value: 'key2', type: 'emoji'}]
};
panelView = new SwipeablePanelView(layout, {}, viewManager);
panelView.render();
var section = panelView.sections[0];
var keys = section.querySelectorAll('button.emoji');
assert.equal(keys.length, 2);
var dummyElement = section.querySelector('span');
assert.equal(dummyElement.style.flexGrow, 4);
});
test('> render() one section at a time', function() {
var layout = {
panelKeys: []
};
for (var i = 0; i < 20; i++) {
layout.panelKeys.push({value: 'key' + i, type: 'emoji'});
}
panelView = new SwipeablePanelView(layout, {}, viewManager);
panelView.render();
var keys = panelView.sections[0].querySelectorAll('button.emoji');
assert.equal(keys.length, 18);
keys = panelView.sections[1].querySelectorAll('button.emoji');
assert.equal(keys.length, 2);
assert.equal(keys[0].textContent, 'key18');
});
});
suite('panning', function() {
var rootElement = document.createElement('div');
setup(function() {
document.body.appendChild(rootElement);
var options = {
totalWidth: 320
};
panelView = new SwipeablePanelView(layout, options,
viewManager);
panelView.render();
});
teardown(function() {
rootElement.innerHTML = '';
});
test('> panning the first section - forward', function() {
assert.equal(panelView.sections[0].style.transform, '');
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: -20,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-20px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(300px)');
});
test('> panning the first section - backward', function() {
assert.equal(panelView.sections[0].style.transform, '');
panelView._handleTouchStart({
position: {clientX: 0, clientY: 0}
});
panelView._handlePan({
dx: 20,
position: {clientX: 20, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, '');
});
test('> panning the second section - forward', function() {
panelView.gotoSection(1);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: -20,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[1].style.transform, 'translateX(-20px)');
assert.equal(panelView.sections[2].style.transform, 'translateX(300px)');
});
test('> panning the second section - backward', function() {
panelView.gotoSection(1);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: 20,
position: {clientX: 40, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-300px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(20px)');
});
test('> panning the last section - forward', function() {
panelView.gotoSection(sectionCount - 1);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: -20,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[sectionCount - 1].style.transform,
'translateX(0px)');
});
test('> panning the last section - backward', function() {
panelView.gotoSection(sectionCount - 1);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: 20,
position: {clientX: 40, clientY: 0}
});
assert.equal(panelView.sections[sectionCount - 1].style.transform,
'translateX(20px)');
assert.equal(panelView.sections[sectionCount - 2].style.transform,
'translateX(-300px)');
});
test('> swiping through the first section - short distance', function() {
rootElement.appendChild(panelView.element);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: -20,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-20px)');
panelView._handleSwipe({
direction: 'left',
vx: 0.3
});
assert.equal(panelView.sections[0].style.transform, 'translateX(0px)');
});
test('> swiping through the first section - far enough', function() {
rootElement.appendChild(panelView.element);
panelView._handleTouchStart({
position: {clientX: 100, clientY: 0}
});
panelView._handlePan({
dx: -100,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-100px)');
panelView._handleSwipe({
direction: 'left',
vx: -0.3
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-320px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(0px)');
});
test('> swiping through the first section - fast enough', function() {
rootElement.appendChild(panelView.element);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: -20,
position: {clientX: 0, clientY: 0}
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-20px)');
panelView._handleSwipe({
direction: 'left',
vx: -1
});
assert.equal(panelView.sections[0].style.transform, 'translateX(-320px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(0px)');
});
test('> swiping back to the first section, > threshold', function() {
rootElement.appendChild(panelView.element);
panelView.gotoSection(1);
panelView._handleTouchStart({
position: {clientX: 20, clientY: 0}
});
panelView._handlePan({
dx: 100,
position: {clientX: 120, clientY: 0}
});
assert.equal(panelView.sections[1].style.transform, 'translateX(100px)');
panelView._handleSwipe({
direction: 'right',
vx: 0.3
});
assert.equal(panelView.sections[0].style.transform, 'translateX(0px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(320px)');
});
});
suite('rotate', function() {
var rootElement = document.createElement('div');
setup(function() {
document.body.appendChild(rootElement);
var options = { totalWidth: 320 };
panelView = new SwipeablePanelView(layout, options,
viewManager);
panelView.render();
});
teardown(function() {
rootElement.innerHTML = '';
});
test('> rotate to landscape - at first section', function() {
panelView.resize(590);
assert.equal(panelView.sections[0].style.transform, 'translateX(0px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(590px)');
});
test('> rotate to landscape - at second section', function() {
panelView.gotoSection(1);
panelView.resize(590);
assert.equal(panelView.sections[0].style.transform, 'translateX(-590px)');
assert.equal(panelView.sections[1].style.transform, 'translateX(0px)');
assert.equal(panelView.sections[2].style.transform, 'translateX(590px)');
});
test('> rotate to landscape - at last section', function() {
panelView.gotoSection(sectionCount - 1);
panelView.resize(590);
assert.equal(
panelView.sections[sectionCount - 1].style.transform,
'translateX(0px)');
assert.equal(
panelView.sections[sectionCount - 2].style.transform,
'translateX(-590px)');
});
});
});

View file

@ -0,0 +1,161 @@
'use strict';
/* global SwipingDetector */
require('/js/views/swiping_detector.js');
suite('View utils > SwipingDetector', function() {
function fakeTouchDispatch(type, panel, xs, ys) {
var touches = [];
for (var i = 0; i < xs.length; i++) {
var x = xs[i];
var y = ys[i];
var touch = document.createTouch(window, panel, 42, x, y,
x, y, x, y,
0, 0, 0, 0);
touches.push(touch);
}
var touchList = document.createTouchList(touches);
var eventTouches = (type == 'touchstart' || type == 'touchmove') ?
touchList : null;
var eventChanged = (type == 'touchmove') ?
null : touchList;
var e = document.createEvent('TouchEvent');
e.initTouchEvent(type, true, true,
null, null, false, false, false, false,
eventTouches, null, eventChanged);
panel.dispatchEvent(e);
return e;
}
function touchStart(panel, xs, ys) {
return fakeTouchDispatch('touchstart', panel, xs, ys);
}
function touchMove(panel, xs, ys) {
return fakeTouchDispatch('touchmove', panel, xs, ys);
}
function touchEnd(panel, xs, ys) {
return fakeTouchDispatch('touchend', panel, xs, ys);
}
function swipe(clock, panel, fromX, toX, fromY, toY, duration, noEnd) {
var events = [];
duration = duration || 350;
events.push(touchStart(panel, [fromX], [fromY]));
var diffX = Math.abs(toX - fromX);
var diffY = Math.abs(toY - fromY);
var delta = Math.max(diffX, diffY);
var step = step || 1;
var x = 0, y = 0;
var tick = duration / delta;
for (var i = 0; i < delta; i++) {
var newX = fromX + x;
var newY = fromY + y;
events.push(touchMove(panel, [newX], [newY]));
clock.tick(tick);
if (newX < toX) {
x++;
}
if (newX > toX) {
x--;
}
if (newY < toY) {
y++;
}
if (newY > toY) {
y--;
}
}
if (!noEnd) {
events.push(touchEnd(panel, [toX], [toY]));
}
return events;
}
function createFakeCoordinates(x, y, timeStamp, id) {
return Object.freeze({
screenX: x,
screenY: y,
clientX: x,
clientY: y,
timeStamp: timeStamp,
identifier: id
});
}
var swipingDetector = null;
var element = null;
setup(function() {
element = document.createElement('div');
swipingDetector = new SwipingDetector(element);
this.sinon.useFakeTimers();
});
test('init', function() {
swipingDetector.start();
});
test('pan', function() {
swipingDetector.start();
var panHandler = this.sinon.stub();
swipingDetector.onpan = panHandler;
swipe(this.sinon.clock, element, 21, 0, 0, 0, 100, true);
assert.equal(panHandler.lastCall.args[0].dx, -20);
});
test('swipe', function() {
swipingDetector.start();
var swipeHandler = this.sinon.stub();
swipingDetector.onswipe = swipeHandler;
swipe(this.sinon.clock, element, 0, 21, 0, 0, 100);
assert.equal(swipeHandler.firstCall.args[0].direction, 'right');
swipe(this.sinon.clock, element, 20, 0, 0, 0, 300);
assert.equal(swipeHandler.lastCall.args[0].direction, 'left');
});
test('swipe will report velocity', function() {
swipingDetector.start();
var swipeHandler = this.sinon.stub();
swipingDetector.onswipe = swipeHandler;
swipingDetector.handleEvent({
type: 'touchstart',
touches: [createFakeCoordinates(0, 0, 0)],
timeStamp: 0
});
swipingDetector.handleEvent({
type: 'touchmove',
touches: [createFakeCoordinates(20, 0, 0)],
timeStamp: 100
});
swipingDetector.handleEvent({
type: 'touchend',
changedTouches: [createFakeCoordinates(20, 0, 0)],
timeStamp: 100
});
assert.equal(swipeHandler.firstCall.args[0].vx, 0.2);
});
});