change(web): element-wrapper modularization + unit test conversion

This commit is contained in:
Joshua A. Horton 2023-01-11 14:12:09 +07:00
parent 62cdf6a054
commit fc84af4842
38 changed files with 2656 additions and 2494 deletions

View file

@ -72,8 +72,8 @@ module.exports = {
proxies: {
"/resources/": "/base/common/test/resources/",
"/node_modules/": "/base/node_modules/",
"/@keymanapp/lm-worker/": "/base/node_modules/@keymanapp/lm-worker/"
// "/node_modules/": "/base/node_modules/",
// "/@keymanapp/lm-worker/": "/base/node_modules/@keymanapp/lm-worker/"
},
// web server port

View file

@ -59,5 +59,6 @@
"@keymanapp/web-utils": "*",
"@types/node": "^11.9.4",
"eventemitter3": "^4.0.0"
}
},
"type": "module"
}

View file

@ -0,0 +1,24 @@
/*
* Note: while this file is not meant to exist long-term, it provides a nice
* low-level proof-of-concept for esbuild bundling of the various Web submodules.
*
* Add some extra code at the end of src/index.ts and run it to verify successful bundling!
*/
import esbuild from 'esbuild';
import { spawn } from 'child_process';
await esbuild.build({
bundle: true,
sourcemap: true,
format: "esm",
nodePaths: ['../../../../node_modules'],
entryPoints: {
'index': '../../../build/engine/device-detect/obj/kmwdevice.js',
},
external: ['fs', 'vm'],
outdir: '../../../build/engine/device-detect/lib/',
outExtension: { '.js': '.mjs' },
tsconfig: './tsconfig.json',
target: "es5"
});

View file

@ -1,9 +1,9 @@
import StyleConstants from 'utils/styleConstants.js';
import StyleConstants from './utils/styleConstants.js';
import { DeviceSpec, Version } from "@keymanapp/web-utils/build/obj/index.js";
// The Device object definition -------------------------------------------------
export class Device {
export default class Device {
touchable: boolean;
OS: string;
formFactor: string;

View file

@ -0,0 +1,24 @@
/*
* Note: while this file is not meant to exist long-term, it provides a nice
* low-level proof-of-concept for esbuild bundling of the various Web submodules.
*
* Add some extra code at the end of src/index.ts and run it to verify successful bundling!
*/
import esbuild from 'esbuild';
import { spawn } from 'child_process';
await esbuild.build({
bundle: true,
sourcemap: true,
format: "esm",
nodePaths: ['../../../../node_modules'],
entryPoints: {
'index': '../../../build/engine/element-wrappers/obj/index.js',
},
external: ['fs', 'vm'],
outdir: '../../../build/engine/element-wrappers/lib/',
outExtension: { '.js': '.mjs' },
tsconfig: './tsconfig.json',
target: "es5"
});

View file

@ -1,8 +1,4 @@
## engine/element-wrappers
This submodule provides a subset of the main engine's Web-oriented code that's used to 'wrap' webpage
elements as part of KMW attachment and interface the element with the `keyboard-processor` submodule.
At this time, said code is included as part of KMW's main source. It is provided here as a
separate module for use in unit tests, though it is 100% engine code. It is not currently practical
to 100% modularize it in KMW's current state due to cross-references.
elements as part of KMW attachment and interface the element with the `keyboard-processor` submodule.

View file

@ -0,0 +1,267 @@
import OutputTarget from './outputTarget.js';
class SelectionCaret {
node: Node;
offset: number;
constructor(node, offset) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
export default class ContentEditable extends OutputTarget {
root: HTMLElement;
constructor(ele: HTMLElement) {
if(ele.isContentEditable) {
super();
this.root = ele;
} else {
throw "Specified element is not already content-editable!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLElement {
return this.root;
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.root.ownerDocument.getSelection().isCollapsed;
}
hasSelection(): boolean {
let Lsel = this.root.ownerDocument.getSelection();
if(this.root != Lsel.anchorNode && !this.root.contains(Lsel.anchorNode)) {
return false;
}
if(this.root != Lsel.focusNode && !this.root.contains(Lsel.focusNode)) {
return false;
}
return true;
}
clearSelection(): void {
if(this.hasSelection()) {
let Lsel = this.root.ownerDocument.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
let Lsel = this.root.ownerDocument.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return this.getTextBeforeCaret().kmwLength();
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
let caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.root.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
let start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
let range = this.root.ownerDocument.createRange();
let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let start = this.getCarets().start;
let delta = s._kmwLength();
let Lsel = this.root.ownerDocument.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
let finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
let textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
var n = start.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
let delta = s._kmwLength();
let Lsel = this.root.ownerDocument.getSelection();
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
let textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
var n = caret.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -0,0 +1,352 @@
import OutputTarget from './outputTarget.js';
class SelectionCaret {
node: Node;
offset: number;
constructor(node, offset) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
class StyleCommand {
cmd: string;
stateType: number;
cache: string|boolean;
constructor(c: string, s:number) {
this.cmd = c;
this.stateType = s;
}
}
export default class DesignIFrame extends OutputTarget {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;
commandCache: StyleCommand[];
constructor(ele: HTMLIFrameElement) {
super();
this.root = ele;
if(ele.contentWindow && ele.contentWindow.document && ele.contentWindow.document.designMode == 'on') {
this.doc = ele.contentWindow.document;
this.docRoot = ele.contentWindow.document.documentElement;
} else {
throw "Specified IFrame is not in design-mode!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLIFrameElement {
return this.root;
}
focus(): void {
this.doc.defaultView.focus(); // I3363 (Build 301)
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.doc.getSelection().isCollapsed;
}
hasSelection(): boolean {
let Lsel = this.doc.getSelection();
let outerSel = document.getSelection();
// If the outer doc's selection matches, we're active.
if(outerSel.anchorNode == Lsel.anchorNode && outerSel.focusNode == Lsel.focusNode) {
return true;
} else {
// Problem: for testing, we can't enforce the ideal (ie: first) condition.
// Technically, the IFrame _will_ always have its own internal selection, though... so... it kinda works?
return true;
}
}
clearSelection(): void {
if(this.hasSelection()) {
let Lsel = this.doc.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
let Lsel = this.doc.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return this.getTextBeforeCaret().kmwLength();
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
let caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.docRoot.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
let start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
let range = this.doc.createRange();
let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let start = this.getCarets().start;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
let finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
let textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
var n = this.doc.createTextNode(s);
let range = this.doc.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
let textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
var n = caret.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
/**
* Function saveProperties
* Scope Private
* Description Build and create list of styles that can be applied in iframes
*/
saveProperties() {
// Formerly _CacheCommands.
var _CacheableCommands=[
new StyleCommand('backcolor',1), new StyleCommand('fontname',1), new StyleCommand('fontsize',1),
new StyleCommand('forecolor',1), new StyleCommand('bold',0), new StyleCommand('italic',0),
new StyleCommand('strikethrough',0), new StyleCommand('subscript',0),
new StyleCommand('superscript',0), new StyleCommand('underline',0)
];
if(this.doc.defaultView) {
_CacheableCommands.push(new StyleCommand('hilitecolor',1));
}
for(var n=0; n < _CacheableCommands.length; n++) { // I1511 - array prototype extended
let cmd = _CacheableCommands[n];
//KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]);
if(cmd.stateType == 1) {
cmd.cache = this.doc.queryCommandValue(cmd.cmd);
} else {
cmd.cache = this.doc.queryCommandState(cmd.cmd);
}
}
this.commandCache = _CacheableCommands;
}
/**
* Function restoreProperties
* Scope Private
* Description Restore styles in IFRAMEs (??)
*/
restoreProperties(_func?: () => void): void {
// Formerly _CacheCommandsReset.
if(!this.commandCache) {
console.error("No command cache exists to restore!");
}
for(var n=0; n < this.commandCache.length; n++) { // I1511 - array prototype extended
let cmd = this.commandCache[n];
//KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]);
if(cmd.stateType == 1) {
if(this.doc.queryCommandValue(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
this.doc.execCommand(cmd.cmd, false, <string> cmd.cache);
}
} else if(this.doc.queryCommandState(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
//KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]);
this.doc.execCommand(cmd.cmd, false, null);
}
}
}
doInputEvent() {
// Root = the iframe, the outermost component and the one we were originally told to attach to.
this.dispatchInputEventOn(this.root);
}
}

View file

@ -0,0 +1,7 @@
export { default as wrapElement } from './wrapElement.js';
export { default as ContentEditable } from './contentEditable.js';
export { default as DesignIFrame } from './designIFrame.js';
export { default as Input } from './input.js';
export { default as OutputTarget } from './outputTarget.js';
export { default as TextArea } from './textarea.js';
export { nestedInstanceOf } from './utils.js';

View file

@ -0,0 +1,233 @@
import OutputTarget, { BaseEventMap } from './outputTarget.js';
interface EventMap extends BaseEventMap {
/**
* Used to facilitate a pre-modularization utility method we wish to maintain:
```
export function forceScroll(element: HTMLInputElement | HTMLTextAreaElement) {
// Only executes when com.keyman.DOMEventHandlers is defined.
//
// We bypass this whenever operating in the embedded format.
if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) {
let DOMEventHandlers = com.keyman['DOMEventHandlers'];
let selectionStart = element.selectionStart;
let selectionEnd = element.selectionEnd;
DOMEventHandlers.states._IgnoreBlurFocus = true;
//Forces scrolling; the re-focus triggers the scroll, at least.
element.blur();
element.focus();
DOMEventHandlers.states._IgnoreBlurFocus = false;
// On Edge, it appears that the blur/focus combination will reset the caret position
// under certain scenarios during unit tests. So, we re-set it afterward.
element.selectionStart = selectionStart;
element.selectionEnd = selectionEnd;
}
}
```
* References to the event-handlers & related states objects are not available within this submodule.
*
* It is the parts between and including the _IgnoreBlurFocus references that must be
* implemented externally.
*/
'scrollfocusrequest': (element: HTMLInputElement) => void,
/**
* This event will be raised when a newline is received by wrapped elements not of
* the 'search' or 'submit' types.
*
* Original code this is replacing:
```
// Allows compiling this separately from the main body of KMW.
// TODO: rework class to accept a class-static 'callback' from the DOM module that this can call.
// Would eliminate the need for this 'static' reference.
// Only strongly matters once we better modularize KMW, with web-dom vs web-dom-targets vs web-core, etc.
if(com.keyman["singleton"]) {
com.keyman["singleton"].domManager.moveToNext(false);
}
```
* This does not belong in a modularized version of this class; it must be supplied
* by the consuming top-level products instead.
*/
'unhandlednewline': (element: HTMLInputElement) => void
}
export default class Input extends OutputTarget<EventMap> {
root: HTMLInputElement;
/**
* Tracks the most recently-cached selection start index.
*/
private _cachedSelectionStart: number
/**
* Tracks the most recently processed, extended-string-based selection start index.
* When the element's selectionStart value changes, this should be invalidated.
*/
private processedSelectionStart: number;
/**
* Tracks the most recently processed, extended-string-based selection end index.
* When the element's selectionEnd value changes, this should be invalidated.
*/
private processedSelectionEnd: number;
constructor(ele: HTMLInputElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
// Intended to facilitate reimplmentation of the old `forceScroll` as an event handler
// defined externally, but automatically set on class construction.
Input.constructorExtensions(this);
}
/**
* This may be set to define additional construction behaviors to perform, such as
* automatically setting handlers for defined events.
*/
public static constructorExtensions: (constructingInstance: Input) => void = () => {};
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLInputElement {
return this.root;
}
clearSelection(): void {
// Processes our codepoint-based variants of selectionStart and selectionEnd.
this.getCaret(); // updates processedSelectionStart if required
this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319
this.setCaret(this.processedSelectionStart);
}
isSelectionEmpty(): boolean {
return this.root.selectionStart == this.root.selectionEnd;
}
hasSelection(): boolean {
return true;
}
invalidateSelection() {
// Since .selectionStart will never return this value, we use it to indicate
// the need to refresh our processed indices.
this._cachedSelectionStart = -1;
}
getCaret(): number {
if(this.root.selectionStart != this._cachedSelectionStart) {
this._cachedSelectionStart = this.root.selectionStart; // KMW-1
this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319
this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319
}
return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;
}
getDeadkeyCaret(): number {
return this.getCaret();
}
setCaret(caret: number) {
this.setSelection(caret, caret, "none");
}
setSelection(start: number, end: number, direction: "forward" | "backward" | "none") {
let domStart = this.root.value._kmwCodePointToCodeUnit(start);
let domEnd = this.root.value._kmwCodePointToCodeUnit(end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
this.events.emit('scrollfocusrequest', this.root);
this.root.setSelectionRange(domStart, domEnd, direction);
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
let direction = this.getSelectionDirection();
let newCaret = text._kmwLength();
this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
let c = this.getCaret();
let direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
let curText = this.getTextBeforeCaret();
let caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
let caret = this.getCaret();
let front = this.getTextBeforeCaret();
let back = this.getText()._kmwSubstring(this.processedSelectionStart);
this.adjustDeadkeys(s._kmwLength());
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
const inputEle = this.root;
// Can't occur for Mocks - just Input types.
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
this.events.emit('unhandlednewline', inputEle);
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -0,0 +1,62 @@
import OutputTargetBase from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js";
import EventEmitter from 'eventemitter3';
export interface BaseEventMap {
/**
* Meant to facilitate the following code that existed pre-modularization:
```
// This class has non-integrated unit tests in which the `singleton` object doesn't exist.
// Thus, we need to test for this case.
let keyman = com.keyman['singleton'];
// Signal the necessary text changes to the embedding app, if it exists.
if(keyman && keyman['oninserttext'] && keyman.isEmbedded) {
keyman['oninserttext'](transform.deleteLeft, transform.insert, transform.deleteRight);
}
```
*/
'oninserttext': (deleteLeft: number, insert: string, deleteRight: number) => void;
}
export default abstract class OutputTarget<EventMap extends BaseEventMap = BaseEventMap> extends OutputTargetBase {
// JS/TS can't do multiple inheritance, so we maintain class events on a readonly field.
public readonly events: EventEmitter<EventMap, this> = new EventEmitter<EventMap, this>();
/**
* Returns the underlying element / document modeled by the wrapper.
*/
abstract getElement(): HTMLElement;
public focus(): void {
const ele = this.getElement();
if(ele.focus) {
ele.focus();
}
}
/**
* A helper method for doInputEvent; creates a simple common event and default dispatching.
* @param elem
*/
protected dispatchInputEventOn(elem: HTMLElement) {
let event: InputEvent;
// `undefined` in pre-Chrome Edge and Chrome for Android before version 60.
if(window['InputEvent']) { // can't condition on the type directly; TS optimizes that out.
event = new InputEvent('input', {"bubbles": true, "cancelable": false});
}
if(elem && event) {
elem.dispatchEvent(event);
}
}
apply(transform: Transform) {
super.apply(transform);
// The TS compiler can't quite handle this typing scenario properly; the following cast
// allows us to work around its type inference limitations.
const baseEvents = (this.events as unknown as EventEmitter<BaseEventMap, this>);
baseEvents.emit('oninserttext', transform.deleteLeft, transform.insert, transform.deleteRight);
}
}

View file

@ -0,0 +1,216 @@
import OutputTarget, { BaseEventMap } from './outputTarget.js';
interface EventMap extends BaseEventMap {
/**
* Used to facilitate a pre-modularization utility method we wish to maintain:
```
export function forceScroll(element: HTMLInputElement | HTMLTextAreaElement) {
// Only executes when com.keyman.DOMEventHandlers is defined.
//
// We bypass this whenever operating in the embedded format.
if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) {
let DOMEventHandlers = com.keyman['DOMEventHandlers'];
let selectionStart = element.selectionStart;
let selectionEnd = element.selectionEnd;
DOMEventHandlers.states._IgnoreBlurFocus = true;
//Forces scrolling; the re-focus triggers the scroll, at least.
element.blur();
element.focus();
DOMEventHandlers.states._IgnoreBlurFocus = false;
// On Edge, it appears that the blur/focus combination will reset the caret position
// under certain scenarios during unit tests. So, we re-set it afterward.
element.selectionStart = selectionStart;
element.selectionEnd = selectionEnd;
}
}
```
* References to the event-handlers & related states objects are not available within this submodule.
*
* It is the parts between and including the _IgnoreBlurFocus references that must be
* implemented externally.
*/
'scrollfocusrequest': (element: HTMLTextAreaElement) => void,
}
export default class TextArea extends OutputTarget<EventMap> {
root: HTMLTextAreaElement;
/**
* Tracks the most recently-cached selection start index.
*/
private _cachedSelectionStart: number
/**
* Tracks the most recently processed, extended-string-based selection start index.
* When the element's selectionStart value changes, this should be invalidated.
*/
private processedSelectionStart: number;
/**
* Tracks the most recently processed, extended-string-based selection end index.
* When the element's selectionEnd value changes, this should be invalidated.
*/
private processedSelectionEnd: number;
/**
* Used to temporarily store the y-axis scroll coordinate.
*/
private scrollTop?: number;
/**
* Used to temporarily store the x-axis scroll coordinate.
*/
private scrollLeft?: number;
constructor(ele: HTMLTextAreaElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
// Intended to facilitate reimplmentation of the old `forceScroll` as an event handler
// defined externally, but automatically set on class construction.
TextArea.constructorExtensions(this);
}
/**
* This may be set to define additional construction behaviors to perform, such as
* automatically setting handlers for defined events.
*/
public static constructorExtensions: (constructingInstance: TextArea) => void = () => {};
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLTextAreaElement {
return this.root;
}
clearSelection(): void {
// Processes our codepoint-based variants of selectionStart and selectionEnd.
this.getCaret(); // updates processedSelectionStart if required
this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319
this.setCaret(this.processedSelectionStart);
}
isSelectionEmpty(): boolean {
return this.root.selectionStart == this.root.selectionEnd;
}
hasSelection(): boolean {
return true;
}
invalidateSelection() {
// Since .selectionStart will never return this value, we use it to indicate
// the need to refresh our processed indices.
this._cachedSelectionStart = -1;
}
getCaret(): number {
if(this.root.selectionStart != this._cachedSelectionStart) {
this._cachedSelectionStart = this.root.selectionStart; // KMW-1
this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319
this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319
}
return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;
}
getDeadkeyCaret(): number {
return this.getCaret();
}
setCaret(caret: number) {
this.setSelection(caret, caret, "none");
}
setSelection(start: number, end: number, direction: "forward" | "backward" | "none") {
let domStart = this.root.value._kmwCodePointToCodeUnit(start);
let domEnd = this.root.value._kmwCodePointToCodeUnit(end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
this.events.emit('scrollfocusrequest', this.root);
this.root.setSelectionRange(domStart, domEnd, direction);
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
let direction = this.getSelectionDirection();
let newCaret = text._kmwLength();
this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
let c = this.getCaret();
let direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
let curText = this.getTextBeforeCaret();
let caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
let caret = this.getCaret();
let front = this.getTextBeforeCaret();
let back = this.getText()._kmwSubstring(this.processedSelectionStart);
this.adjustDeadkeys(s._kmwLength());
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -0,0 +1,43 @@
/**
* Checks the type of an input DOM-related object while ensuring that it is checked against the correct prototype,
* as class prototypes are (by specification) scoped upon the owning Window.
*
* See https://stackoverflow.com/questions/43587286/why-does-instanceof-return-false-on-chrome-safari-and-edge-and-true-on-firefox
* for more details.
*
* @param {Element|Event} Pelem An element of the web page or one of its IFrame-based subdocuments.
* @param {string} className The plain-text name of the expected Element type.
* @return {boolean}
*/
export function nestedInstanceOf(Pelem: Event|EventTarget, className: string): boolean {
var scopedClass;
if(!Pelem) {
// If we're bothering to check something's type, null references don't match
// what we're looking for.
return false;
}
if (Pelem['Window']) { // Window objects contain the class definitions for types held within them. So, we can check for those.
return className == 'Window';
} else if (Pelem['defaultView']) { // Covers Document.
scopedClass = Pelem['defaultView'][className];
} else if(Pelem['ownerDocument']) {
scopedClass = (Pelem as Node).ownerDocument.defaultView[className];
} else if(Pelem['target']) {
var event = Pelem as Event;
if(this.instanceof(event.target, 'Window')) {
scopedClass = event.target[className];
} else if(this.instanceof(event.target, 'Document')) {
scopedClass = (event.target as Document).defaultView[className];
} else if(this.instanceof(event.target, 'HTMLElement')) {
scopedClass = (event.target as HTMLElement).ownerDocument.defaultView[className];
}
}
if(scopedClass) {
return Pelem instanceof scopedClass;
} else {
return false;
}
}

View file

@ -0,0 +1,31 @@
import type OutputTarget from './outputTarget.js';
import Input from './input.js';
import TextArea from './textarea.js';
import DesignIFrame from './designIFrame.js';
import ContentEditable from './contentEditable.js';
import { nestedInstanceOf } from './utils.js';
export default function wrapElement(e: HTMLElement): OutputTarget<any> {
// Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.
if(nestedInstanceOf(e, "HTMLInputElement")) {
return new Input(<HTMLInputElement> e);
} else if(nestedInstanceOf(e, "HTMLTextAreaElement")) {
return new TextArea(<HTMLTextAreaElement> e);
} else if(nestedInstanceOf(e, "HTMLIFrameElement")) {
let iframe = <HTMLIFrameElement> e;
if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == "on") {
return new DesignIFrame(iframe);
} else if (e.isContentEditable) {
// Do content-editable <iframe>s make sense?
return new ContentEditable(e);
} else {
return null;
}
} else if(e.isContentEditable) {
return new ContentEditable(e);
}
return null;
}

View file

@ -1,13 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outFile": "../../../build/engine/element-wrappers/obj/index.bundled.js"
},
"references": [
{ "path": "../../../../common/web/keyman-version", "prepend": true },
{ "path": "../../../../common/web/utils", "prepend": true },
{ "path": "../../../../common/web/keyboard-processor/src", "prepend": true }
]
}

View file

@ -4,28 +4,21 @@
"compilerOptions": {
"allowJs": true,
"inlineSources": true,
"module": "none",
"outFile": "../../../build/engine/element-wrappers/obj/index.js",
"allowSyntheticDefaultImports": true,
"module": "es6",
"moduleResolution": "Node",
"sourceMap": true,
"target": "es5"
"target": "es5",
"outDir": "../../../build/engine/element-wrappers/obj/",
"tsBuildInfoFile": "../../../build/engine/element-wrappers/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"files": [
"../main/dom/targets/wrapElement.ts",
"../main/dom/targets/input.ts",
"../main/dom/targets/textarea.ts",
"../main/dom/targets/contentEditable.ts",
"../main/dom/targets/designIFrame.ts",
"../main/dom/targets/outputTarget.ts",
"../main/kmwexthtml.ts",
"../main/dom/utils.ts",
"../main/kmwtypedefs.ts",
"../../../../node_modules/eventemitter3/index.js"
],
"include": [ "src/**/*.ts" ],
"references": [
{ "path": "../../../../common/web/keyman-version" },
{ "path": "../../../../common/web/utils" },
{ "path": "../../../../common/web/keyboard-processor/src" }
// { "path": "../../../../common/web/keyman-version" },
// { "path": "../../../../common/web/utils" },
{ "path": "../../../../common/web/keyboard-processor" }
]
}

View file

@ -1,267 +0,0 @@
namespace com.keyman.dom.targets {
class SelectionCaret {
node: Node;
offset: number;
constructor(node, offset) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
export class ContentEditable extends OutputTarget {
root: HTMLElement;
constructor(ele: HTMLElement) {
if(ele.isContentEditable) {
super();
this.root = ele;
} else {
throw "Specified element is not already content-editable!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLElement {
return this.root;
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.root.ownerDocument.getSelection().isCollapsed;
}
hasSelection(): boolean {
let Lsel = this.root.ownerDocument.getSelection();
if(this.root != Lsel.anchorNode && !this.root.contains(Lsel.anchorNode)) {
return false;
}
if(this.root != Lsel.focusNode && !this.root.contains(Lsel.focusNode)) {
return false;
}
return true;
}
clearSelection(): void {
if(this.hasSelection()) {
let Lsel = this.root.ownerDocument.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
let Lsel = this.root.ownerDocument.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return this.getTextBeforeCaret().kmwLength();
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
let caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.root.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
let start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
let range = this.root.ownerDocument.createRange();
let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let start = this.getCarets().start;
let delta = s._kmwLength();
let Lsel = this.root.ownerDocument.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
let finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
let textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
var n = start.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
let delta = s._kmwLength();
let Lsel = this.root.ownerDocument.getSelection();
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
let textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
var n = caret.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}
}

View file

@ -1,352 +0,0 @@
namespace com.keyman.dom.targets {
class SelectionCaret {
node: Node;
offset: number;
constructor(node, offset) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
class StyleCommand {
cmd: string;
stateType: number;
cache: string|boolean;
constructor(c: string, s:number) {
this.cmd = c;
this.stateType = s;
}
}
export class DesignIFrame extends OutputTarget {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;
commandCache: StyleCommand[];
constructor(ele: HTMLIFrameElement) {
super();
this.root = ele;
if(ele.contentWindow && ele.contentWindow.document && ele.contentWindow.document.designMode == 'on') {
this.doc = ele.contentWindow.document;
this.docRoot = ele.contentWindow.document.documentElement;
} else {
throw "Specified IFrame is not in design-mode!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLIFrameElement {
return this.root;
}
focus(): void {
this.doc.defaultView.focus(); // I3363 (Build 301)
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.doc.getSelection().isCollapsed;
}
hasSelection(): boolean {
let Lsel = this.doc.getSelection();
let outerSel = document.getSelection();
// If the outer doc's selection matches, we're active.
if(outerSel.anchorNode == Lsel.anchorNode && outerSel.focusNode == Lsel.focusNode) {
return true;
} else {
// Problem: for testing, we can't enforce the ideal (ie: first) condition.
// Technically, the IFrame _will_ always have its own internal selection, though... so... it kinda works?
return true;
}
}
clearSelection(): void {
if(this.hasSelection()) {
let Lsel = this.doc.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
let Lsel = this.doc.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return this.getTextBeforeCaret().kmwLength();
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
let caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.docRoot.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
let start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
let range = this.doc.createRange();
let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let start = this.getCarets().start;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
let finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
let textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
var n = this.doc.createTextNode(s);
let range = this.doc.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
let textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
var n = caret.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
/**
* Function saveProperties
* Scope Private
* Description Build and create list of styles that can be applied in iframes
*/
saveProperties() {
// Formerly _CacheCommands.
var _CacheableCommands=[
new StyleCommand('backcolor',1), new StyleCommand('fontname',1), new StyleCommand('fontsize',1),
new StyleCommand('forecolor',1), new StyleCommand('bold',0), new StyleCommand('italic',0),
new StyleCommand('strikethrough',0), new StyleCommand('subscript',0),
new StyleCommand('superscript',0), new StyleCommand('underline',0)
];
if(this.doc.defaultView) {
_CacheableCommands.push(new StyleCommand('hilitecolor',1));
}
for(var n=0; n < _CacheableCommands.length; n++) { // I1511 - array prototype extended
let cmd = _CacheableCommands[n];
//KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]);
if(cmd.stateType == 1) {
cmd.cache = this.doc.queryCommandValue(cmd.cmd);
} else {
cmd.cache = this.doc.queryCommandState(cmd.cmd);
}
}
this.commandCache = _CacheableCommands;
}
/**
* Function restoreProperties
* Scope Private
* Description Restore styles in IFRAMEs (??)
*/
restoreProperties(_func?: () => void): void {
// Formerly _CacheCommandsReset.
if(!this.commandCache) {
console.error("No command cache exists to restore!");
}
for(var n=0; n < this.commandCache.length; n++) { // I1511 - array prototype extended
let cmd = this.commandCache[n];
//KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]);
if(cmd.stateType == 1) {
if(this.doc.queryCommandValue(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
this.doc.execCommand(cmd.cmd, false, <string> cmd.cache);
}
} else if(this.doc.queryCommandState(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
//KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]);
this.doc.execCommand(cmd.cmd, false, null);
}
}
}
doInputEvent() {
// Root = the iframe, the outermost component and the one we were originally told to attach to.
this.dispatchInputEventOn(this.root);
}
}
}

View file

@ -1,177 +0,0 @@
namespace com.keyman.dom.targets {
export class Input extends OutputTarget {
root: HTMLInputElement;
/**
* Tracks the most recently-cached selection start index.
*/
private _cachedSelectionStart: number
/**
* Tracks the most recently processed, extended-string-based selection start index.
* When the element's selectionStart value changes, this should be invalidated.
*/
private processedSelectionStart: number;
/**
* Tracks the most recently processed, extended-string-based selection end index.
* When the element's selectionEnd value changes, this should be invalidated.
*/
private processedSelectionEnd: number;
constructor(ele: HTMLInputElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLInputElement {
return this.root;
}
clearSelection(): void {
// Processes our codepoint-based variants of selectionStart and selectionEnd.
this.getCaret(); // updates processedSelectionStart if required
this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319
this.setCaret(this.processedSelectionStart);
}
isSelectionEmpty(): boolean {
return this.root.selectionStart == this.root.selectionEnd;
}
hasSelection(): boolean {
return true;
}
invalidateSelection() {
// Since .selectionStart will never return this value, we use it to indicate
// the need to refresh our processed indices.
this._cachedSelectionStart = -1;
}
getCaret(): number {
if(this.root.selectionStart != this._cachedSelectionStart) {
this._cachedSelectionStart = this.root.selectionStart; // KMW-1
this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319
this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319
}
return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;
}
getDeadkeyCaret(): number {
return this.getCaret();
}
setCaret(caret: number) {
this.setSelection(caret, caret, "none");
}
setSelection(start: number, end: number, direction: "forward" | "backward" | "none") {
let domStart = this.root.value._kmwCodePointToCodeUnit(start);
let domEnd = this.root.value._kmwCodePointToCodeUnit(end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
Utils.forceScroll(this.root);
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
let direction = this.getSelectionDirection();
let newCaret = text._kmwLength();
this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
let c = this.getCaret();
let direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
let curText = this.getTextBeforeCaret();
let caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
let caret = this.getCaret();
let front = this.getTextBeforeCaret();
let back = this.getText()._kmwSubstring(this.processedSelectionStart);
this.adjustDeadkeys(s._kmwLength());
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
Input.newlineHandler(this.root);
}
static newlineHandler(inputEle: HTMLInputElement) {
// Can't occur for Mocks - just Input types.
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
// Allows compiling this separately from the main body of KMW.
// TODO: rework class to accept a class-static 'callback' from the DOM module that this can call.
// Would eliminate the need for this 'static' reference.
// Only strongly matters once we better modularize KMW, with web-dom vs web-dom-targets vs web-core, etc.
if(com.keyman["singleton"]) {
com.keyman["singleton"].domManager.moveToNext(false);
}
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}
}

View file

@ -1,45 +0,0 @@
namespace com.keyman.dom.targets {
export abstract class OutputTarget extends text.OutputTarget {
/**
* Returns the underlying element / document modeled by the wrapper.
*/
abstract getElement(): HTMLElement;
public focus(): void {
const ele = this.getElement();
if(ele.focus) {
ele.focus();
}
}
/**
* A helper method for doInputEvent; creates a simple common event and default dispatching.
* @param elem
*/
protected dispatchInputEventOn(elem: HTMLElement) {
let event: InputEvent;
// `undefined` in pre-Chrome Edge and Chrome for Android before version 60.
if(window['InputEvent']) { // can't condition on the type directly; TS optimizes that out.
event = new InputEvent('input', {"bubbles": true, "cancelable": false});
}
if(elem && event) {
elem.dispatchEvent(event);
}
}
apply(transform: Transform) {
super.apply(transform);
// This class has non-integrated unit tests in which the `singleton` object doesn't exist.
// Thus, we need to test for this case.
let keyman = com.keyman['singleton'];
// Signal the necessary text changes to the embedding app, if it exists.
if(keyman && keyman['oninserttext'] && keyman.isEmbedded) {
keyman['oninserttext'](transform.deleteLeft, transform.insert, transform.deleteRight);
}
}
}
}

View file

@ -1,170 +0,0 @@
namespace com.keyman.dom.targets {
export class TextArea extends OutputTarget {
root: HTMLTextAreaElement;
/**
* Tracks the most recently-cached selection start index.
*/
private _cachedSelectionStart: number
/**
* Tracks the most recently processed, extended-string-based selection start index.
* When the element's selectionStart value changes, this should be invalidated.
*/
private processedSelectionStart: number;
/**
* Tracks the most recently processed, extended-string-based selection end index.
* When the element's selectionEnd value changes, this should be invalidated.
*/
private processedSelectionEnd: number;
/**
* Used to temporarily store the y-axis scroll coordinate.
*/
private scrollTop?: number;
/**
* Used to temporarily store the x-axis scroll coordinate.
*/
private scrollLeft?: number;
constructor(ele: HTMLTextAreaElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLTextAreaElement {
return this.root;
}
clearSelection(): void {
// Processes our codepoint-based variants of selectionStart and selectionEnd.
this.getCaret(); // updates processedSelectionStart if required
this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319
this.setCaret(this.processedSelectionStart);
}
isSelectionEmpty(): boolean {
return this.root.selectionStart == this.root.selectionEnd;
}
hasSelection(): boolean {
return true;
}
invalidateSelection() {
// Since .selectionStart will never return this value, we use it to indicate
// the need to refresh our processed indices.
this._cachedSelectionStart = -1;
}
getCaret(): number {
if(this.root.selectionStart != this._cachedSelectionStart) {
this._cachedSelectionStart = this.root.selectionStart; // KMW-1
this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319
this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319
}
return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;
}
getDeadkeyCaret(): number {
return this.getCaret();
}
setCaret(caret: number) {
this.setSelection(caret, caret, "none");
}
setSelection(start: number, end: number, direction: "forward" | "backward" | "none") {
let domStart = this.root.value._kmwCodePointToCodeUnit(start);
let domEnd = this.root.value._kmwCodePointToCodeUnit(end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
Utils.forceScroll(this.root);
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
let direction = this.getSelectionDirection();
let newCaret = text._kmwLength();
this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
let c = this.getCaret();
let direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
let curText = this.getTextBeforeCaret();
let caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
let caret = this.getCaret();
let front = this.getTextBeforeCaret();
let back = this.getText()._kmwSubstring(this.processedSelectionStart);
this.adjustDeadkeys(s._kmwLength());
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}
}

View file

@ -1,36 +0,0 @@
/// <reference path="outputTarget.ts" />
// Defines a basic HTMLInputElement wrapper.
///<reference path="input.ts" />
// Defines a basic HTMLTextAreaElement wrapper.
///<reference path="textarea.ts" />
// Defines a basic content-editable wrapper.
///<reference path="contentEditable.ts" />
// Defines a basic design-mode IFrame wrapper.
///<reference path="designIFrame.ts" />
namespace com.keyman.dom.targets {
export function wrapElement(e: HTMLElement): OutputTarget {
// Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.
if(Utils.instanceof(e, "HTMLInputElement")) {
return new Input(<HTMLInputElement> e);
} else if(Utils.instanceof(e, "HTMLTextAreaElement")) {
return new TextArea(<HTMLTextAreaElement> e);
} else if(Utils.instanceof(e, "HTMLIFrameElement")) {
let iframe = <HTMLIFrameElement> e;
if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == "on") {
return new DesignIFrame(iframe);
} else if (e.isContentEditable) {
// Do content-editable <iframe>s make sense?
return new ContentEditable(e);
} else {
return null;
}
} else if(e.isContentEditable) {
return new ContentEditable(e);
}
return null;
}
}

View file

@ -1,4 +1,7 @@
namespace com.keyman.dom {
// NOTE:
// - instanceOf -> element-wrappers, now called nestedInstanceOf
// - forceScroll -> element-wrappers, but I believe it's only ever called from there.
// Defines DOM-related utility functions that are not reliant on KMW's internal state.
export class Utils {
@ -109,73 +112,5 @@ namespace com.keyman.dom {
}
return Lcurtop;
}
/**
* Checks the type of an input DOM-related object while ensuring that it is checked against the correct prototype,
* as class prototypes are (by specification) scoped upon the owning Window.
*
* See https://stackoverflow.com/questions/43587286/why-does-instanceof-return-false-on-chrome-safari-and-edge-and-true-on-firefox
* for more details.
*
* @param {Element|Event} Pelem An element of the web page or one of its IFrame-based subdocuments.
* @param {string} className The plain-text name of the expected Element type.
* @return {boolean}
*/
static instanceof(Pelem: Event|EventTarget, className: string): boolean {
var scopedClass;
if(!Pelem) {
// If we're bothering to check something's type, null references don't match
// what we're looking for.
return false;
}
if (Pelem['Window']) { // Window objects contain the class definitions for types held within them. So, we can check for those.
return className == 'Window';
} else if (Pelem['defaultView']) { // Covers Document.
scopedClass = Pelem['defaultView'][className];
} else if(Pelem['ownerDocument']) {
scopedClass = (Pelem as Node).ownerDocument.defaultView[className];
} else if(Pelem['target']) {
var event = Pelem as Event;
if(this.instanceof(event.target, 'Window')) {
scopedClass = event.target[className];
} else if(this.instanceof(event.target, 'Document')) {
scopedClass = (event.target as Document).defaultView[className];
} else if(this.instanceof(event.target, 'HTMLElement')) {
scopedClass = (event.target as HTMLElement).ownerDocument.defaultView[className];
}
}
if(scopedClass) {
return Pelem instanceof scopedClass;
} else {
return false;
}
}
static forceScroll(element: HTMLInputElement | HTMLTextAreaElement) {
// Needed to allow ./build_dev_resources.sh to complete;
// only executes when com.keyman.DOMEventHandlers is defined.
//
// We also bypass this whenever operating in the embedded format.
if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) {
let DOMEventHandlers = com.keyman['DOMEventHandlers'];
let selectionStart = element.selectionStart;
let selectionEnd = element.selectionEnd;
DOMEventHandlers.states._IgnoreBlurFocus = true;
//Forces scrolling; the re-focus triggers the scroll, at least.
element.blur();
element.focus();
DOMEventHandlers.states._IgnoreBlurFocus = false;
// On Edge, it appears that the blur/focus combination will reset the caret position
// under certain scenarios during unit tests. So, we re-set it afterward.
element.selectionStart = selectionStart;
element.selectionEnd = selectionEnd;
}
}
}
}

View file

@ -2,7 +2,10 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outFile": "../../../build/engine/main/obj/keymanweb.js"
"outFile": "../../../build/engine/main/obj/keymanweb.js",
"allowSyntheticDefaultImports": true,
"module": "es6",
"moduleResolution": "Node",
},
"include": [
@ -20,9 +23,9 @@
"references": [
{ "path": "../../../../common/web/keyman-version"},
{ "path": "../../../../common/web/utils"},
{ "path": "../../../../common/predictive-text/browser.tsconfig.json"},
{ "path": "../../../../common/web/input-processor/src"},
{ "path": "../../../../common/web/keyboard-processor/src"},
{ "path": "../../../../common/predictive-text"},
{ "path": "../../../../common/web/input-processor"},
{ "path": "../../../../common/web/keyboard-processor"},
{ "path": "../../../../common/web/lm-message-types" },
{ "path": "../device-detect" }
]

View file

@ -38,15 +38,28 @@ module.exports = {
// list of files / patterns to load in the browser
files: [
'web/src/test/auto/modernizr.js', // A dependency-managed utility script that helps with browser feature detection.
'web/build/engine/element-wrappers/obj/index.bundled.js', // Defines com.keyman.dom objects separate from KMW for unit testing.
'web/build/engine/device-detect/obj/index.bundled.js', // Defines com.keyman.utils.Device, separated from KMW for use in unit test setup.
'web/build/tools/testing/recorder/obj/index.js', // The object definitions used to generate/replicate key events for engine tests.
// 'web/build/engine/element-wrappers/obj/index.bundled.js', // Defines com.keyman.dom objects separate from KMW for unit testing.
// 'web/build/engine/device-detect/obj/index.bundled.js', // Defines com.keyman.utils.Device, separated from KMW for use in unit test setup.
// 'web/build/tools/testing/recorder/obj/index.js', // The object definitions used to generate/replicate key events for engine tests.
// Includes KMW's Device class, which is used by test_utils below.
'web/src/test/auto/test_init_check.js', // Ensures that tests will initialize properly
{pattern: 'web/src/test/auto/test_init_check.js', type: 'module'}, // Ensures that tests will initialize properly
'common/test/resources/timeout-adapter.js', // Handles configuration timeout setup at runtime.
'web/src/test/auto/test_utils.js', // A basic utility script useful for constructing tests
'web/src/test/auto/cases/**/*.js', // Where the tests actually reside.
{pattern: 'web/src/test/auto/test_utils.js', type: 'module'}, // A basic utility script useful for constructing tests
{pattern: 'web/src/test/auto/cases/**/*.js', type: 'module'}, // Where the tests actually reside.
'common/test/resources/json/**/*.json', // Where pre-loaded JSON resides.
{pattern: 'web/build/**/*.js', watched: true, served: true, included: false}, // Includes all top-level KMW products
{pattern: 'web/build/**/*.js.map', watched: true, served: true, included: false}, // and their sourcemaps.
{pattern: 'web/build/**/*.mjs', watched: true, served: true, included: false}, // Includes all top-level KMW products
{pattern: 'web/build/**/*.mjs.map', watched: true, served: true, included: false}, // and their sourcemaps.
{ pattern: 'common/predictive-text/build/obj/**/*.*', watched: true, served: true, included: false },
{ pattern: 'common/predictive-text/build/obj/**/*.js.map', watched: true, served: true, included: false },
// { pattern: 'common/web/lm-worker/build/lib/*.js', watched: true, served: true, included: false},
// { pattern: 'common/web/lm-worker/build/lib/*.js.map', watched: true, served: true, included: false},
{pattern: 'common/web/**/*.js', watched: true, served: true, included: false},
{pattern: 'common/web/**/*.js.map', watched: true, served: true, included: false},
{pattern: 'common/test/resources/fixtures/**/*.html', watched: true}, // HTML structures useful for testing.
{pattern: 'common/test/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources.
{pattern: 'web/build/app/web/debug/**/*.css', watched: false, served: true, included: false}, // OSK resources
@ -66,7 +79,11 @@ module.exports = {
"/source/": "/base/web/build/app/web/debug/",
"/ui-source/": "/base/web/build/app/ui/debug/",
"/resources/": "/base/common/test/resources/",
"/source/recorder_InputEvents.js.map": "/base/common/tests/recorder_InputEvents.js.map"
"/source/recorder_InputEvents.js.map": "/base/common/tests/recorder_InputEvents.js.map",
"/node_modules/": "/base/node_modules/",
"/@keymanapp/web-utils/": "/base/common/web/utils/",
"/@keymanapp/keyman/": "/base/web/",
"/@keymanapp/keyboard-processor/": "/base/common/web/keyboard-processor/"
},

View file

@ -1,257 +1,257 @@
var assert = chai.assert;
// var assert = chai.assert;
describe('Attachment API', function() {
this.timeout(testconfig.timeouts.standard);
// describe('Attachment API', function() {
// this.timeout(testconfig.timeouts.standard);
before(function() {
assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
fixture.setBase('fixtures');
// before(function() {
// assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
// fixture.setBase('fixtures');
this.timeout(testconfig.timeouts.scriptLoad * 3);
return setupKMW({ attachType:'manual' }, testconfig.timeouts.scriptLoad).then(() => {
const kbd1 = loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad, { passive: true });
const kbd2 = loadKeyboardFromJSON("/keyboards/khmer_angkor.json", testconfig.timeouts.scriptLoad, { passive: true });
return Promise.all([kbd1, kbd2]).then(() => {
return keyman.setActiveKeyboard("lao_2008_basic", "lo");
});
});
});
// this.timeout(testconfig.timeouts.scriptLoad * 3);
// return setupKMW({ attachType:'manual' }, testconfig.timeouts.scriptLoad).then(() => {
// const kbd1 = loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad, { passive: true });
// const kbd2 = loadKeyboardFromJSON("/keyboards/khmer_angkor.json", testconfig.timeouts.scriptLoad, { passive: true });
// return Promise.all([kbd1, kbd2]).then(() => {
// return keyman.setActiveKeyboard("lao_2008_basic", "lo");
// });
// });
// });
after(function() {
keyman.removeKeyboards('lao_2008_basic');
keyman.removeKeyboards('khmer_angkor');
teardownKMW();
});
// after(function() {
// keyman.removeKeyboards('lao_2008_basic');
// keyman.removeKeyboards('khmer_angkor');
// teardownKMW();
// });
beforeEach(function() {
fixture.load("robustAttachment.html");
});
// beforeEach(function() {
// fixture.load("robustAttachment.html");
// });
afterEach(function(done) {
fixture.cleanup();
window.setTimeout(function(){
done();
}, testconfig.timeouts.eventDelay);
});
// afterEach(function(done) {
// fixture.cleanup();
// window.setTimeout(function(){
// done();
// }, testconfig.timeouts.eventDelay);
// });
it("Attachment/Detachment", function(done) {
// Since we're in 'manual', we start detached.
var ele = document.getElementById(DynamicElements.addInput());
// it("Attachment/Detachment", function(done) {
// // Since we're in 'manual', we start detached.
// var ele = document.getElementById(DynamicElements.addInput());
window.setTimeout(function() {
// Ensure we didn't auto-attach.
DynamicElements.assertDetached(ele);
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(DynamicElements.keyCommand);
// window.setTimeout(function() {
// // Ensure we didn't auto-attach.
// DynamicElements.assertDetached(ele);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
var val = ele.value;
ele.value = "";
assert.equal(val, DynamicElements.disabledOutput, "'Detached' element performed keystroke processing!");
// var val = ele.value;
// ele.value = "";
// assert.equal(val, DynamicElements.disabledOutput, "'Detached' element performed keystroke processing!");
keyman.attachToControl(ele);
DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment.
// keyman.attachToControl(ele);
// DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment.
eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(DynamicElements.keyCommand);
// eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(ele);
// val = retrieveAndReset(ele);
assert.equal(val, DynamicElements.enabledLaoOutput, "'Attached' element did not perform keystroke processing!");
// assert.equal(val, DynamicElements.enabledLaoOutput, "'Attached' element did not perform keystroke processing!");
done();
}, testconfig.timeouts.eventDelay);
});
// done();
// }, testconfig.timeouts.eventDelay);
// });
it("Enablement/Disablement", function(done) {
// Since we're in 'manual', we start detached.
var ele = document.getElementById(DynamicElements.addInput());
window.setTimeout(function() {
keyman.attachToControl(ele);
keyman.disableControl(ele);
// it("Enablement/Disablement", function(done) {
// // Since we're in 'manual', we start detached.
// var ele = document.getElementById(DynamicElements.addInput());
// window.setTimeout(function() {
// keyman.attachToControl(ele);
// keyman.disableControl(ele);
// It appears that mobile devices do not instantly trigger the MutationObserver, so we need a small timeout
// for the change to take effect.
window.setTimeout(function() {
DynamicElements.assertAttached(ele);
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(ele);
assert.equal(val, DynamicElements.disabledOutput, "'Disabled' element performed keystroke processing!");
// // It appears that mobile devices do not instantly trigger the MutationObserver, so we need a small timeout
// // for the change to take effect.
// window.setTimeout(function() {
// DynamicElements.assertAttached(ele);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(ele);
// assert.equal(val, DynamicElements.disabledOutput, "'Disabled' element performed keystroke processing!");
keyman.enableControl(ele);
window.setTimeout(function() {
DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment.
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(ele);
assert.equal(val, DynamicElements.enabledLaoOutput, "'Enabled' element did not perform keystroke processing!");
done();
}, testconfig.timeouts.eventDelay);
}, testconfig.timeouts.eventDelay);
}, testconfig.timeouts.eventDelay);
});
// keyman.enableControl(ele);
// window.setTimeout(function() {
// DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment.
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(ele);
// assert.equal(val, DynamicElements.enabledLaoOutput, "'Enabled' element did not perform keystroke processing!");
// done();
// }, testconfig.timeouts.eventDelay);
// }, testconfig.timeouts.eventDelay);
// }, testconfig.timeouts.eventDelay);
// });
it("Keyboard Management (active control)", function() {
// It appears that event generation + inline event dispatching is a bit time-intensive on some browsers.
this.timeout(testconfig.timeouts.standard * 2);
// it("Keyboard Management (active control)", function() {
// // It appears that event generation + inline event dispatching is a bit time-intensive on some browsers.
// this.timeout(testconfig.timeouts.standard * 2);
var input = document.getElementById(DynamicElements.addInput());
var textarea = document.getElementById(DynamicElements.addText());
// var input = document.getElementById(DynamicElements.addInput());
// var textarea = document.getElementById(DynamicElements.addText());
keyman.attachToControl(input);
keyman.attachToControl(textarea);
// keyman.attachToControl(input);
// keyman.attachToControl(textarea);
keyman.setActiveElement(input);
// We assume from the other tests that running on the Lao keyboard will give proper output.
// It'd be a redundant check.
// keyman.setActiveElement(input);
// // We assume from the other tests that running on the Lao keyboard will give proper output.
// // It'd be a redundant check.
// Set control with independent keyboard.
keyman.setKeyboardForControl(input, "khmer_angkor", "km");
var eventDriver = new KMWRecorder.BrowserDriver(input);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(input);
assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not use control's keyboard settings!");
// // Set control with independent keyboard.
// keyman.setKeyboardForControl(input, "khmer_angkor", "km");
// var eventDriver = new KMWRecorder.BrowserDriver(input);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(input);
// assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not use control's keyboard settings!");
// Swap to a global-linked control...
keyman.setActiveElement(textarea);
eventDriver = new KMWRecorder.BrowserDriver(textarea);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(textarea);
assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not use manage keyboard settings correctly for global-linked control!");
// // Swap to a global-linked control...
// keyman.setActiveElement(textarea);
// eventDriver = new KMWRecorder.BrowserDriver(textarea);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(textarea);
// assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not use manage keyboard settings correctly for global-linked control!");
// Swap back and check that the settings persist.
keyman.setActiveElement(input);
eventDriver = new KMWRecorder.BrowserDriver(input);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(input);
assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW forgot control's independent keyboard settings!");
// // Swap back and check that the settings persist.
// keyman.setActiveElement(input);
// eventDriver = new KMWRecorder.BrowserDriver(input);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(input);
// assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW forgot control's independent keyboard settings!");
// Finally, clear the independent setting.
keyman.setKeyboardForControl(input, null, null);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(input);
assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!");
});
// // Finally, clear the independent setting.
// keyman.setKeyboardForControl(input, null, null);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(input);
// assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!");
// });
it("Keyboard Management (inactive control)", function() {
// It appears that event generation + inline event dispatching is a bit time-intensive on some browsers.
this.timeout(testconfig.timeouts.standard * 2);
// it("Keyboard Management (inactive control)", function() {
// // It appears that event generation + inline event dispatching is a bit time-intensive on some browsers.
// this.timeout(testconfig.timeouts.standard * 2);
var input = document.getElementById(DynamicElements.addInput());
var textarea = document.getElementById(DynamicElements.addText());
// var input = document.getElementById(DynamicElements.addInput());
// var textarea = document.getElementById(DynamicElements.addText());
keyman.attachToControl(input);
keyman.attachToControl(textarea);
// keyman.attachToControl(input);
// keyman.attachToControl(textarea);
// We assume from the other tests that running on the Lao keyboard will give proper output.
// It'd be a redundant check.
// // We assume from the other tests that running on the Lao keyboard will give proper output.
// // It'd be a redundant check.
// We are testing that setting a specific keyboard for an inactive control does not affect
// the currently active control. The textarea control will be manually set to Khmer,
// and the input control will get the document default of Lao.
// // We are testing that setting a specific keyboard for an inactive control does not affect
// // the currently active control. The textarea control will be manually set to Khmer,
// // and the input control will get the document default of Lao.
keyman.setActiveElement(input);
// Set textarea control with independent keyboard khmer_angkor.
keyman.setKeyboardForControl(textarea, "khmer_angkor", "km");
var eventDriver = new KMWRecorder.BrowserDriver(input);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(input);
assert.equal(val, DynamicElements.enabledLaoOutput, "KMW set independent keyboard for the incorrect control!");
// keyman.setActiveElement(input);
// // Set textarea control with independent keyboard khmer_angkor.
// keyman.setKeyboardForControl(textarea, "khmer_angkor", "km");
// var eventDriver = new KMWRecorder.BrowserDriver(input);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(input);
// assert.equal(val, DynamicElements.enabledLaoOutput, "KMW set independent keyboard for the incorrect control!");
// Swap to the textarea control with its overridden khmer_angkor keyboard...
keyman.setActiveElement(textarea);
eventDriver = new KMWRecorder.BrowserDriver(textarea);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(textarea);
assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not properly store keyboard for the previously-inactive control!");
// // Swap to the textarea control with its overridden khmer_angkor keyboard...
// keyman.setActiveElement(textarea);
// eventDriver = new KMWRecorder.BrowserDriver(textarea);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(textarea);
// assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not properly store keyboard for the previously-inactive control!");
// Swap back to the input control and check that the settings persist.
keyman.setActiveElement(input);
keyman.setKeyboardForControl(textarea, null, null);
// // Swap back to the input control and check that the settings persist.
// keyman.setActiveElement(input);
// keyman.setKeyboardForControl(textarea, null, null);
eventDriver = new KMWRecorder.BrowserDriver(input);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(input);
assert.equal(val, DynamicElements.enabledLaoOutput, "KMW made a strange error when clearing an inactive control's keyboard setting!");
// eventDriver = new KMWRecorder.BrowserDriver(input);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(input);
// assert.equal(val, DynamicElements.enabledLaoOutput, "KMW made a strange error when clearing an inactive control's keyboard setting!");
keyman.setActiveElement(textarea);
// Finally, after clearing the independent setting, check that we are back to Lao output as expected for the textarea
eventDriver = new KMWRecorder.BrowserDriver(textarea);
eventDriver.simulateEvent(DynamicElements.keyCommand);
val = retrieveAndReset(textarea);
assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!");
});
});
// keyman.setActiveElement(textarea);
// // Finally, after clearing the independent setting, check that we are back to Lao output as expected for the textarea
// eventDriver = new KMWRecorder.BrowserDriver(textarea);
// eventDriver.simulateEvent(DynamicElements.keyCommand);
// val = retrieveAndReset(textarea);
// assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!");
// });
// });
describe('Attachment Checks (Desktop, \'auto\')', function() {
// describe('Attachment Checks (Desktop, \'auto\')', function() {
this.timeout(testconfig.timeouts.standard);
// this.timeout(testconfig.timeouts.standard);
before(function() {
this.timeout(testconfig.timeouts.scriptLoad);
// before(function() {
// this.timeout(testconfig.timeouts.scriptLoad);
fixture.setBase('fixtures');
return setupKMW({ attachType:'auto' }, testconfig.timeouts.scriptLoad);
});
// fixture.setBase('fixtures');
// return setupKMW({ attachType:'auto' }, testconfig.timeouts.scriptLoad);
// });
beforeEach(function() {
fixture.load("robustAttachment.html");
});
// beforeEach(function() {
// fixture.load("robustAttachment.html");
// });
after(function() {
teardownKMW();
});
// after(function() {
// teardownKMW();
// });
afterEach(function(done) {
fixture.cleanup();
window.setTimeout(function(){
done();
}, testconfig.timeouts.eventDelay);
})
// afterEach(function(done) {
// fixture.cleanup();
// window.setTimeout(function(){
// done();
// }, testconfig.timeouts.eventDelay);
// })
describe('Element Type', function() {
it('<input>', function(done) {
var ID = DynamicElements.addInput();
var ele = document.getElementById(ID);
// describe('Element Type', function() {
// it('<input>', function(done) {
// var ID = DynamicElements.addInput();
// var ele = document.getElementById(ID);
DynamicElements.assertAttached(ele, done);
});
// DynamicElements.assertAttached(ele, done);
// });
it('<textarea>', function(done) {
var ID = DynamicElements.addText();
var ele = document.getElementById(ID);
// it('<textarea>', function(done) {
// var ID = DynamicElements.addText();
// var ele = document.getElementById(ID);
DynamicElements.assertAttached(ele, done);
});
// DynamicElements.assertAttached(ele, done);
// });
it.skip('<iframe>', function(done) {
this.timeout(testconfig.timeouts.scriptLoad * 2); // Just in case, for iframe loading time.
// it.skip('<iframe>', function(done) {
// this.timeout(testconfig.timeouts.scriptLoad * 2); // Just in case, for iframe loading time.
var ID = DynamicElements.addIFrame(function() {
var ele = document.getElementById(ID);
var innerEle = ele.contentDocument.getElementById('iframe_input');
// var ID = DynamicElements.addIFrame(function() {
// var ele = document.getElementById(ID);
// var innerEle = ele.contentDocument.getElementById('iframe_input');
// No need to track data on the iframe itself.
assert.isFalse(keyman.isAttached(ele));
assert.isNotNull(innerEle);
assert.isTrue(keyman.isAttached(innerEle));
keyman.detachFromControl(ele);
// // No need to track data on the iframe itself.
// assert.isFalse(keyman.isAttached(ele));
// assert.isNotNull(innerEle);
// assert.isTrue(keyman.isAttached(innerEle));
// keyman.detachFromControl(ele);
window.setTimeout(function() {
done();
}, testconfig.timeouts.eventDelay);
});
});
// window.setTimeout(function() {
// done();
// }, testconfig.timeouts.eventDelay);
// });
// });
// If we were to set up a design-mode iframe test, that one would need to be conditioned on
// whether or not we were on a touch-device, at least as of this time. (#7343)
// // If we were to set up a design-mode iframe test, that one would need to be conditioned on
// // whether or not we were on a touch-device, at least as of this time. (#7343)
it('contentEditable=true', function(done) {
var ID = DynamicElements.addEditable();
var ele = document.getElementById(ID);
// it('contentEditable=true', function(done) {
// var ID = DynamicElements.addEditable();
// var ele = document.getElementById(ID);
DynamicElements.assertAttached(ele, done);
});
});
});
// DynamicElements.assertAttached(ele, done);
// });
// });
// });

View file

@ -1,154 +1,154 @@
var assert = chai.assert;
// var assert = chai.assert;
describe('Basic KeymanWeb', function() {
this.timeout(testconfig.timeouts.standard);
// describe('Basic KeymanWeb', function() {
// this.timeout(testconfig.timeouts.standard);
before(function() {
// These tests require use of KMW's device-detection functionality.
assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
})
// before(function() {
// // These tests require use of KMW's device-detection functionality.
// assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
// })
beforeEach(function() {
this.timeout(testconfig.timeouts.scriptLoad);
// beforeEach(function() {
// this.timeout(testconfig.timeouts.scriptLoad);
fixture.setBase('fixtures');
fixture.load("singleInput.html");
return setupKMW(null, testconfig.timeouts.scriptLoad);
});
// fixture.setBase('fixtures');
// fixture.load("singleInput.html");
// return setupKMW(null, testconfig.timeouts.scriptLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
describe('Initialization', function() {
it('KMW should attach to the input element.', function() {
var singleton = document.getElementById('singleton');
assert.isTrue(keyman.isAttached(singleton), "KeymanWeb did not automatically attach to the element!");
});
it('KMW\'s initialization variable should indicate completion.', function() {
assert(keyman.initialized == 2, 'Keyman indicates incomplete initialization!');
});
});
});
// describe('Initialization', function() {
// it('KMW should attach to the input element.', function() {
// var singleton = document.getElementById('singleton');
// assert.isTrue(keyman.isAttached(singleton), "KeymanWeb did not automatically attach to the element!");
// });
// it('KMW\'s initialization variable should indicate completion.', function() {
// assert(keyman.initialized == 2, 'Keyman indicates incomplete initialization!');
// });
// });
// });
Modernizr.on('touchevents', function(result) {
if(!result) {
describe('Basic Toggle UI', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// Modernizr.on('touchevents', function(result) {
// if(!result) {
// describe('Basic Toggle UI', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
beforeEach(function() {
this.timeout(testconfig.timeouts.uiLoad);
fixture.setBase('fixtures');
fixture.load('singleInput.html');
// beforeEach(function() {
// this.timeout(testconfig.timeouts.uiLoad);
// fixture.setBase('fixtures');
// fixture.load('singleInput.html');
// Loads two scripts in parallel, but just in case, 2x timeout.
return setupKMW('toggle', testconfig.timeouts.uiLoad);
});
// // Loads two scripts in parallel, but just in case, 2x timeout.
// return setupKMW('toggle', testconfig.timeouts.uiLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
it('The Toggle UI initializes correctly.', function() {
assert(keyman.ui.initialized, 'Initialization flag is set to false!');
// it('The Toggle UI initializes correctly.', function() {
// assert(keyman.ui.initialized, 'Initialization flag is set to false!');
assert.isNotNull(keyman.ui.controller, 'Failed to create the controller element!');
// assert.isNotNull(keyman.ui.controller, 'Failed to create the controller element!');
var divs = document.getElementsByTagName("div");
var match = false;
// var divs = document.getElementsByTagName("div");
// var match = false;
for(var i=0; i < divs.length; i++) {
if(divs[i] == keyman.ui.controller) {
match = true;
}
}
// for(var i=0; i < divs.length; i++) {
// if(divs[i] == keyman.ui.controller) {
// match = true;
// }
// }
assert(match, 'Controller element has not been added to the page!');
})
});
// assert(match, 'Controller element has not been added to the page!');
// })
// });
describe('Basic Button UI', function() {
// describe('Basic Button UI', function() {
beforeEach(function() {
this.timeout(testconfig.timeouts.uiLoad);
fixture.setBase('fixtures');
fixture.load('singleInput.html');
// beforeEach(function() {
// this.timeout(testconfig.timeouts.uiLoad);
// fixture.setBase('fixtures');
// fixture.load('singleInput.html');
// Loads two scripts in parallel, but just in case, 2x timeout.
return setupKMW('button', testconfig.timeouts.uiLoad);
});
// // Loads two scripts in parallel, but just in case, 2x timeout.
// return setupKMW('button', testconfig.timeouts.uiLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
it('The Button UI initializes correctly.', function() {
assert(keyman.ui.init, 'Initialization flag is set to false!');
})
});
// it('The Button UI initializes correctly.', function() {
// assert(keyman.ui.init, 'Initialization flag is set to false!');
// })
// });
describe('Basic Float UI', function() {
// describe('Basic Float UI', function() {
beforeEach(function() {
this.timeout(testconfig.timeouts.uiLoad);
fixture.setBase('fixtures');
fixture.load('singleInput.html');
// beforeEach(function() {
// this.timeout(testconfig.timeouts.uiLoad);
// fixture.setBase('fixtures');
// fixture.load('singleInput.html');
// Loads two scripts in parallel, but just in case, 2x timeout.
return setupKMW('float', testconfig.timeouts.uiLoad);
});
// // Loads two scripts in parallel, but just in case, 2x timeout.
// return setupKMW('float', testconfig.timeouts.uiLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
it('The Float UI initializes correctly.', function() {
assert(keyman.ui.initialized, 'Initialization flag is set to false!');
// it('The Float UI initializes correctly.', function() {
// assert(keyman.ui.initialized, 'Initialization flag is set to false!');
assert.isNotNull(keyman.ui.outerDiv, 'Failed to create the floating controller element!');
// assert.isNotNull(keyman.ui.outerDiv, 'Failed to create the floating controller element!');
var divs = document.getElementsByTagName("div");
var match = false;
// var divs = document.getElementsByTagName("div");
// var match = false;
for(var i=0; i < divs.length; i++) {
if(divs[i] == keyman.ui.outerDiv) {
match = true;
}
}
// for(var i=0; i < divs.length; i++) {
// if(divs[i] == keyman.ui.outerDiv) {
// match = true;
// }
// }
assert(match, 'Floating controller element has not been added to the page!');
})
});
// assert(match, 'Floating controller element has not been added to the page!');
// })
// });
describe('Basic Toolbar UI', function() {
// describe('Basic Toolbar UI', function() {
beforeEach(function() {
this.timeout(testconfig.timeouts.uiLoad);
fixture.setBase('fixtures');
fixture.load('singleInput.html');
// beforeEach(function() {
// this.timeout(testconfig.timeouts.uiLoad);
// fixture.setBase('fixtures');
// fixture.load('singleInput.html');
// Loads two scripts in parallel, but just in case, 2x timeout.
return setupKMW('toolbar', testconfig.timeouts.uiLoad);
});
// // Loads two scripts in parallel, but just in case, 2x timeout.
// return setupKMW('toolbar', testconfig.timeouts.uiLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
it('The Toolbar UI initializes correctly.', function() {
assert(keyman.ui.init, 'Initialization flag is set to false!');
// it('The Toolbar UI initializes correctly.', function() {
// assert(keyman.ui.init, 'Initialization flag is set to false!');
var kwc = document.getElementById('KeymanWebControl');
assert.isNotNull(kwc, 'Toolbar DIV was not added to the page!');
// var kwc = document.getElementById('KeymanWebControl');
// assert.isNotNull(kwc, 'Toolbar DIV was not added to the page!');
var toolbar = document.getElementById('kmw_controls');
assert.isNotNull(toolbar, 'The main toolbar element was not added to the page!');
})
});
}
});
// var toolbar = document.getElementById('kmw_controls');
// assert.isNotNull(toolbar, 'The main toolbar element was not added to the page!');
// })
// });
// }
// });

View file

@ -1,5 +1,14 @@
var assert = chai.assert;
import * as wrappers from '/@keymanapp/keyman/build/engine/element-wrappers/lib/index.mjs';
import { Mock } from '/@keymanapp/keyboard-processor/build/obj/text/outputTarget.js';
import extendString from '../../../../../common/web/utils/build/obj/kmwstring.js';
import Device from '/@keymanapp/keyman/build/engine/device-detect/lib/index.mjs';
import { toSupplementaryPairString, DynamicElements, DEVICE_DETECT_FAILURE } from '../test_utils.js';
extendString();
var InterfaceTests;
// Define common interface testing functions that can be run upon the OutputTarget interface.
@ -24,7 +33,7 @@ if(typeof InterfaceTests == 'undefined') {
InterfaceTests.Input.setupElement = function() {
var id = DynamicElements.addInput();
var elem = document.getElementById(id);
var wrapper = new com.keyman.dom.targets.Input(elem);
var wrapper = new wrappers.Input(elem);
return {elem: elem, wrapper: wrapper};
}
@ -70,7 +79,7 @@ if(typeof InterfaceTests == 'undefined') {
InterfaceTests.TextArea.setupElement = function() {
var id = DynamicElements.addText();
var elem = document.getElementById(id);
var wrapper = new com.keyman.dom.targets.TextArea(elem);
var wrapper = new wrappers.TextArea(elem);
return {elem: elem, wrapper: wrapper};
}
@ -120,7 +129,7 @@ if(typeof InterfaceTests == 'undefined') {
InterfaceTests.ContentEditable.setupElement = function() {
var id = DynamicElements.addEditable();
var elem = document.getElementById(id);
var wrapper = new com.keyman.dom.targets.ContentEditable(elem);
var wrapper = new wrappers.ContentEditable(elem);
return {elem: elem, wrapper: wrapper, node: null};
}
@ -128,7 +137,7 @@ if(typeof InterfaceTests == 'undefined') {
InterfaceTests.ContentEditable.setupDummyElement = function() {
var id = DynamicElements.addEditable();
var elem = document.getElementById(id);
var wrapper = new com.keyman.dom.targets.ContentEditable(elem);
var wrapper = new wrappers.ContentEditable(elem);
return {elem: elem, wrapper: wrapper, node: null};
}
@ -155,7 +164,7 @@ if(typeof InterfaceTests == 'undefined') {
}
InterfaceTests.ContentEditable.setSelectionRange = function(pair, start, end) {
var device = new com.keyman.Device();
var device = new Device();
device.detect();
var node = pair.elem.childNodes[0];
@ -221,11 +230,11 @@ if(typeof InterfaceTests == 'undefined') {
var id1 = DynamicElements.addDesignIFrame(function() {
var id2 = DynamicElements.addDesignIFrame(function() {
elem1 = document.getElementById(id1);
elem2 = document.getElementById(id2);
let elem1 = document.getElementById(id1);
let elem2 = document.getElementById(id2);
obj.mainPair = {elem: elem1, wrapper: new com.keyman.dom.targets.DesignIFrame(elem1), document: elem1.contentWindow.document};
obj.dummyPair = {elem: elem2, wrapper: new com.keyman.dom.targets.DesignIFrame(elem2), document: elem1.contentWindow.document};
obj.mainPair = {elem: elem1, wrapper: new wrappers.DesignIFrame(elem1), document: elem1.contentWindow.document};
obj.dummyPair = {elem: elem2, wrapper: new wrappers.DesignIFrame(elem2), document: elem1.contentWindow.document};
done();
});
@ -262,7 +271,7 @@ if(typeof InterfaceTests == 'undefined') {
}
InterfaceTests.DesignIFrame.setSelectionRange = function(pair, start, end) {
var device = new com.keyman.Device();
var device = new Device();
device.detect();
var node = pair.document.documentElement.childNodes[0];
@ -319,7 +328,7 @@ if(typeof InterfaceTests == 'undefined') {
InterfaceTests.Mock = {};
InterfaceTests.Mock.setupElement = function() {
return {wrapper: new com.keyman.text.Mock()};
return {wrapper: new Mock()};
}
InterfaceTests.Mock.resetWithText = function(pair, string) {
@ -1136,7 +1145,7 @@ describe('Element Input/Output Interfacing', function() {
describe('Wrapper: Content-Editable Elements (using DIVs)', function() {
before(function() {
// These tests require use of KMW's device-detection functionality.
assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
assert.isFalse(DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
})
describe('Caret Handling', function() {
@ -1228,7 +1237,7 @@ describe('Element Input/Output Interfacing', function() {
before(function() {
// These tests require use of KMW's device-detection functionality.
assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
assert.isFalse(DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure.");
})
beforeEach(function(done) {

View file

@ -1,292 +1,292 @@
var assert = chai.assert;
// var assert = chai.assert;
describe('Engine - Browser Interactions', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('Engine - Browser Interactions', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
before(function() {
fixture.setBase('fixtures');
return setupKMW(null, testconfig.timeouts.scriptLoad);
});
// before(function() {
// fixture.setBase('fixtures');
// return setupKMW(null, testconfig.timeouts.scriptLoad);
// });
beforeEach(function(done) {
fixture.load("singleInput.html");
// beforeEach(function(done) {
// fixture.load("singleInput.html");
window.setTimeout(function() {
done()
}, testconfig.timeouts.eventDelay);
});
// window.setTimeout(function() {
// done()
// }, testconfig.timeouts.eventDelay);
// });
after(function() {
teardownKMW();
});
// after(function() {
// teardownKMW();
// });
afterEach(function() {
fixture.cleanup();
});
// afterEach(function() {
// fixture.cleanup();
// });
describe('RegisterStub', function() {
it('RegisterStub on same keyboard twice', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('RegisterStub', function() {
// it('RegisterStub on same keyboard twice', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
var test_callback = function() {
assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
keyman.removeKeyboards('lao_2008_basic');
assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
}
// var test_callback = function() {
// assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
// assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
// keyman.removeKeyboards('lao_2008_basic');
// assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
// }
let finalPromise = loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad)
.then(test_callback);
// let finalPromise = loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad)
// .then(test_callback);
var stub = {
'KI': 'Keyboard_lao_2008_basic',
'KN': 'Lao 2008 Basic',
'KLC': 'lo',
'KL': 'Lao',
'KF': 'resources/keyboards/lao_2008_basic.js'
};
assert.equal(com.keyman.text.KeyboardInterface.prototype.registerStub(stub), 1, "Registering existing keyboard should return 1!");
return finalPromise;
});
// var stub = {
// 'KI': 'Keyboard_lao_2008_basic',
// 'KN': 'Lao 2008 Basic',
// 'KLC': 'lo',
// 'KL': 'Lao',
// 'KF': 'resources/keyboards/lao_2008_basic.js'
// };
// assert.equal(com.keyman.text.KeyboardInterface.prototype.registerStub(stub), 1, "Registering existing keyboard should return 1!");
// return finalPromise;
// });
});
// });
describe('Variable Stores', function() {
this.timeout(testconfig.timeouts.scriptLoad + testconfig.timeouts.standard);
// describe('Variable Stores', function() {
// this.timeout(testconfig.timeouts.scriptLoad + testconfig.timeouts.standard);
beforeEach(function() {
return loadKeyboardFromJSON("/keyboards/options_with_save.json", testconfig.timeouts.scriptLoad);
});
// beforeEach(function() {
// return loadKeyboardFromJSON("/keyboards/options_with_save.json", testconfig.timeouts.scriptLoad);
// });
after(function() {
keyman.removeKeyboards('options_with_save');
fixture.cleanup();
});
// after(function() {
// keyman.removeKeyboards('options_with_save');
// fixture.cleanup();
// });
it('Backing up and restoring (loadStore/saveStore)', function() {
// Keyboard's default value is 0, corresponding to "no foo."
var keyboardID = "options_with_save";
var prefixedKeyboardID = "Keyboard_" + keyboardID;
var storeName = "foo";
// it('Backing up and restoring (loadStore/saveStore)', function() {
// // Keyboard's default value is 0, corresponding to "no foo."
// var keyboardID = "options_with_save";
// var prefixedKeyboardID = "Keyboard_" + keyboardID;
// var storeName = "foo";
return keyman.setActiveKeyboard(keyboardID, 'en').then(function() {
// Alas, saveStore itself requires the keyboard to be active!
KeymanWeb.saveStore(storeName, 1);
// return keyman.setActiveKeyboard(keyboardID, 'en').then(function() {
// // Alas, saveStore itself requires the keyboard to be active!
// KeymanWeb.saveStore(storeName, 1);
// First, ensure that we get the same thing if we load the value immediately.
var value = KeymanWeb.loadStore(prefixedKeyboardID, storeName, 0);
assert.equal(value, 1, "loadStore did not see the value saved to initialize the test before resetting keyboard");
// // First, ensure that we get the same thing if we load the value immediately.
// var value = KeymanWeb.loadStore(prefixedKeyboardID, storeName, 0);
// assert.equal(value, 1, "loadStore did not see the value saved to initialize the test before resetting keyboard");
// Reload the keyboard so that we can test its loaded value.
keyman.removeKeyboards(keyboardID, true);
}).then(() => {
return loadKeyboardFromJSON("/keyboards/options_with_save.json", testconfig.timeouts.scriptLoad);
}).then(() => {
return keyman.setActiveKeyboard(keyboardID, 'en');
}).then(() => {
// This requires proper storage to a cookie, as we'll be on a new instance of the same keyboard.
var value = KeymanWeb.loadStore(prefixedKeyboardID, storeName, 0);
assert.equal(value, 1, "Did not properly save and reload variable store setting");
}).finally(() => {
KeymanWeb.saveStore(storeName, 0);
});
});
// // Reload the keyboard so that we can test its loaded value.
// keyman.removeKeyboards(keyboardID, true);
// }).then(() => {
// return loadKeyboardFromJSON("/keyboards/options_with_save.json", testconfig.timeouts.scriptLoad);
// }).then(() => {
// return keyman.setActiveKeyboard(keyboardID, 'en');
// }).then(() => {
// // This requires proper storage to a cookie, as we'll be on a new instance of the same keyboard.
// var value = KeymanWeb.loadStore(prefixedKeyboardID, storeName, 0);
// assert.equal(value, 1, "Did not properly save and reload variable store setting");
// }).finally(() => {
// KeymanWeb.saveStore(storeName, 0);
// });
// });
it("Multiple-sequence check", function() {
this.timeout(testconfig.timeouts.standard + testconfig.timeouts.scriptLoad * 3);
var keyboardID = "options_with_save";
var storeName = "foo";
// it("Multiple-sequence check", function() {
// this.timeout(testconfig.timeouts.standard + testconfig.timeouts.scriptLoad * 3);
// var keyboardID = "options_with_save";
// var storeName = "foo";
return keyman.setActiveKeyboard(keyboardID, 'en').then(function() {
KeymanWeb.saveStore(storeName, 1);
keyman.removeKeyboards(keyboardID, true);
}).then(() => {
// First test: expects option to be "on" from cookie-init setting, emitting "foo.", then turning option "off".
return runKeyboardTestFromJSON('/engine_tests/options_with_save_1.json',
{usingOSK: false},
assert.equal,
testconfig.timeouts.scriptLoad)
}).then(() => {
// Reset the keyboard... again.
keyman.removeKeyboards(keyboardID, true);
// return keyman.setActiveKeyboard(keyboardID, 'en').then(function() {
// KeymanWeb.saveStore(storeName, 1);
// keyman.removeKeyboards(keyboardID, true);
// }).then(() => {
// // First test: expects option to be "on" from cookie-init setting, emitting "foo.", then turning option "off".
// return runKeyboardTestFromJSON('/engine_tests/options_with_save_1.json',
// {usingOSK: false},
// assert.equal,
// testconfig.timeouts.scriptLoad)
// }).then(() => {
// // Reset the keyboard... again.
// keyman.removeKeyboards(keyboardID, true);
// Second test: expects option to still be "off" b/c cookies.
return runKeyboardTestFromJSON('/engine_tests/options_with_save_2.json',
{usingOSK: false},
assert.equal,
testconfig.timeouts.scriptLoad);
});
});
});
// // Second test: expects option to still be "off" b/c cookies.
// return runKeyboardTestFromJSON('/engine_tests/options_with_save_2.json',
// {usingOSK: false},
// assert.equal,
// testconfig.timeouts.scriptLoad);
// });
// });
// });
// Performs basic processing system checks/tests to ensure the sequence testing
// is based on correct assumptions about the code.
describe('Integrated Simulation Checks', function() {
this.timeout(testconfig.timeouts.standard);
// // Performs basic processing system checks/tests to ensure the sequence testing
// // is based on correct assumptions about the code.
// describe('Integrated Simulation Checks', function() {
// this.timeout(testconfig.timeouts.standard);
before(function() {
this.timeout = testconfig.timeouts.scriptLoad;
return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad);
});
// before(function() {
// this.timeout = testconfig.timeouts.scriptLoad;
// return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad);
// });
beforeEach(function() {
var inputElem = document.getElementById('singleton');
keyman.setActiveElement(inputElem);
inputElem.value = "";
});
// beforeEach(function() {
// var inputElem = document.getElementById('singleton');
// keyman.setActiveElement(inputElem);
// inputElem.value = "";
// });
after(function() {
keyman.removeKeyboards('lao_2008_basic');
fixture.cleanup();
});
// after(function() {
// keyman.removeKeyboards('lao_2008_basic');
// fixture.cleanup();
// });
it('Simple Keypress', function() {
var inputElem = document.getElementById('singleton');
// it('Simple Keypress', function() {
// var inputElem = document.getElementById('singleton');
var lao_s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0};
var lao_s_event = new KMWRecorder.PhysicalInputEventSpec(lao_s_key_json);
// var lao_s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0};
// var lao_s_event = new KMWRecorder.PhysicalInputEventSpec(lao_s_key_json);
let eventDriver = new KMWRecorder.BrowserDriver(inputElem);
eventDriver.simulateEvent(lao_s_event);
// let eventDriver = new KMWRecorder.BrowserDriver(inputElem);
// eventDriver.simulateEvent(lao_s_event);
if(inputElem['base']) {
inputElem = inputElem['base'];
}
assert.equal(inputElem.value, "ຫ");
});
// if(inputElem['base']) {
// inputElem = inputElem['base'];
// }
// assert.equal(inputElem.value, "ຫ");
// });
it('Simple OSK click', function() {
var inputElem = document.getElementById('singleton');
// it('Simple OSK click', function() {
// var inputElem = document.getElementById('singleton');
var lao_s_osk_json = {"type": "osk", "keyID": 'shift-K_S'};
var lao_s_event = new KMWRecorder.OSKInputEventSpec(lao_s_osk_json);
// var lao_s_osk_json = {"type": "osk", "keyID": 'shift-K_S'};
// var lao_s_event = new KMWRecorder.OSKInputEventSpec(lao_s_osk_json);
let eventDriver = new KMWRecorder.BrowserDriver(inputElem);
eventDriver.simulateEvent(lao_s_event);
// let eventDriver = new KMWRecorder.BrowserDriver(inputElem);
// eventDriver.simulateEvent(lao_s_event);
if(inputElem['base']) {
inputElem = inputElem['base'];
}
assert.equal(inputElem.value, ";");
});
})
// if(inputElem['base']) {
// inputElem = inputElem['base'];
// }
// assert.equal(inputElem.value, ";");
// });
// })
describe('Sequence Simulation Checks', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('Sequence Simulation Checks', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
it('Keyboard simulation', function() {
return runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: false}, assert.equal, testconfig.timeouts.scriptLoad);
});
// it('Keyboard simulation', function() {
// return runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: false}, assert.equal, testconfig.timeouts.scriptLoad);
// });
it('OSK simulation', function() {
return runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
})
});
});
// it('OSK simulation', function() {
// return runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
// })
// });
// });
describe('Unmatched Final Groups', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('Unmatched Final Groups', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
before(function() {
fixture.setBase('fixtures');
return setupKMW(null, testconfig.timeouts.scriptLoad + testconfig.timeouts.eventDelay);
});
// before(function() {
// fixture.setBase('fixtures');
// return setupKMW(null, testconfig.timeouts.scriptLoad + testconfig.timeouts.eventDelay);
// });
beforeEach(function(done) {
fixture.load("singleTextArea.html");
// beforeEach(function(done) {
// fixture.load("singleTextArea.html");
window.setTimeout(function() {
done()
}, testconfig.timeouts.eventDelay);
});
// window.setTimeout(function() {
// done()
// }, testconfig.timeouts.eventDelay);
// });
after(function() {
teardownKMW();
});
// after(function() {
// teardownKMW();
// });
afterEach(function() {
fixture.cleanup();
});
// afterEach(function() {
// fixture.cleanup();
// });
it('matches rule from early group AND performs default behavior', function() {
// While a TAB-oriented version would be nice, it's much harder to write the test
// to detect change in last input element.
return runKeyboardTestFromJSON('/engine_tests/ghp_enter.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
});
});
// it('matches rule from early group AND performs default behavior', function() {
// // While a TAB-oriented version would be nice, it's much harder to write the test
// // to detect change in last input element.
// return runKeyboardTestFromJSON('/engine_tests/ghp_enter.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
// });
// });
// Kept separate to maintain an extra-clean setup for this test.
describe('Engine - Browser Interactions', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// // Kept separate to maintain an extra-clean setup for this test.
// describe('Engine - Browser Interactions', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
before(function() {
fixture.setBase('fixtures');
});
// before(function() {
// fixture.setBase('fixtures');
// });
beforeEach(function() {
fixture.load("singleInput.html");
return setupKMW(null, testconfig.timeouts.scriptLoad);
});
// beforeEach(function() {
// fixture.load("singleInput.html");
// return setupKMW(null, testconfig.timeouts.scriptLoad);
// });
afterEach(function() {
fixture.cleanup();
teardownKMW();
});
// afterEach(function() {
// fixture.cleanup();
// teardownKMW();
// });
describe('Keyboard Loading', function() {
it('Local', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('Keyboard Loading', function() {
// it('Local', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json",
testconfig.timeouts.scriptLoad).then(function() {
assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
keyman.removeKeyboards('lao_2008_basic');
assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
});
});
// return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json",
// testconfig.timeouts.scriptLoad).then(function() {
// assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
// assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
// keyman.removeKeyboards('lao_2008_basic');
// assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
// });
// });
it('Automatically sets first available keyboard', function() {
this.timeout(2 * testconfig.timeouts.scriptLoad);
// it('Automatically sets first available keyboard', function() {
// this.timeout(2 * testconfig.timeouts.scriptLoad);
return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json",
testconfig.timeouts.scriptLoad,
{passive: true}).then(() => {
// Because we're loading the keyboard 'passively', KMW's setActiveKeyboard function is auto-called
// on the stub-add. That specific call (for first keyboard auto-activation) is outside of KMW's
// current Promise chain, so we can't _directly_ rely on a KMW Promise to test it.
return new Promise((resolve) => {
let hasResolved = false;
// So, we give KMW the time needed for auto-activation to happen, polling a bit actively so that we don't
// wait unnecessarily long after it occurs.
let absoluteTimer = window.setTimeout(() => {
if(!hasResolved) {
resolve();
hasResolved = true;
}
// return loadKeyboardFromJSON("/keyboards/lao_2008_basic.json",
// testconfig.timeouts.scriptLoad,
// {passive: true}).then(() => {
// // Because we're loading the keyboard 'passively', KMW's setActiveKeyboard function is auto-called
// // on the stub-add. That specific call (for first keyboard auto-activation) is outside of KMW's
// // current Promise chain, so we can't _directly_ rely on a KMW Promise to test it.
// return new Promise((resolve) => {
// let hasResolved = false;
// // So, we give KMW the time needed for auto-activation to happen, polling a bit actively so that we don't
// // wait unnecessarily long after it occurs.
// let absoluteTimer = window.setTimeout(() => {
// if(!hasResolved) {
// resolve();
// hasResolved = true;
// }
window.clearTimeout(intervalTimer);
}, testconfig.timeouts.scriptLoad);
// window.clearTimeout(intervalTimer);
// }, testconfig.timeouts.scriptLoad);
let intervalTimer = window.setInterval(() => {
if(keyman.getActiveKeyboard() != '') {
window.clearTimeout(intervalTimer);
window.clearTimeout(absoluteTimer);
// let intervalTimer = window.setInterval(() => {
// if(keyman.getActiveKeyboard() != '') {
// window.clearTimeout(intervalTimer);
// window.clearTimeout(absoluteTimer);
if(!hasResolved) {
resolve();
hasResolved = true;
}
}
}, 50);
});
// Once this delay-Promise resolves successfully (either way)...
}).then(function() {
assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
keyman.removeKeyboards('lao_2008_basic');
assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
}); // THEN we run our checks.
});
});
});
// if(!hasResolved) {
// resolve();
// hasResolved = true;
// }
// }
// }, 50);
// });
// // Once this delay-Promise resolves successfully (either way)...
// }).then(function() {
// assert.isNotNull(keyman.getKeyboard("lao_2008_basic", "lo"), "Keyboard stub was not registered!");
// assert.equal(keyman.getActiveKeyboard(), "Keyboard_lao_2008_basic", "Keyboard not set automatically!");
// keyman.removeKeyboards('lao_2008_basic');
// assert.equal(keyman.getActiveKeyboard(), '', "Keyboard not removed correctly!");
// }); // THEN we run our checks.
// });
// });
// });

View file

@ -1,47 +1,47 @@
var assert = chai.assert;
// var assert = chai.assert;
describe('Engine - Chirality', function() {
this.timeout(testconfig.timeouts.scriptLoad);
// describe('Engine - Chirality', function() {
// this.timeout(testconfig.timeouts.scriptLoad);
before(function() {
fixture.setBase('fixtures');
return setupKMW(null, testconfig.timeouts.scriptLoad);
});
// before(function() {
// fixture.setBase('fixtures');
// return setupKMW(null, testconfig.timeouts.scriptLoad);
// });
beforeEach(function(done) {
fixture.load("singleInput.html");
// beforeEach(function(done) {
// fixture.load("singleInput.html");
window.setTimeout(function() {
done()
}, testconfig.timeouts.eventDelay);
});
// window.setTimeout(function() {
// done()
// }, testconfig.timeouts.eventDelay);
// });
after(function() {
teardownKMW();
});
// after(function() {
// teardownKMW();
// });
afterEach(function() {
fixture.cleanup();
});
// afterEach(function() {
// fixture.cleanup();
// });
it('Keyboard + OSK simulation', function() {
this.timeout(testconfig.timeouts.scriptLoad * (testconfig.mobile ? 1 : 2));
/* Interestingly, this still works on iOS, probably because we're able to force-set
* the 'location' property in the simulated event on mobile devices, even when iOS neglects to
* set it for real events.
*/
return runKeyboardTestFromJSON('/engine_tests/chirality.json',
{usingOSK: false},
assert.equal,
testconfig.timeouts.scriptLoad).then(() => {
/* We only really care to test the 'desktop' OSK because of how it directly models the modifier keys.
*
* The 'phone' and 'layout' versions take shortcuts that bypass any tricky chiral logic;
* a better test for those would be to ensure the touch OSK is constructed properly.
*/
if(!testconfig.mobile) {
return runKeyboardTestFromJSON('/engine_tests/chirality.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
}
});
});
});
// it('Keyboard + OSK simulation', function() {
// this.timeout(testconfig.timeouts.scriptLoad * (testconfig.mobile ? 1 : 2));
// /* Interestingly, this still works on iOS, probably because we're able to force-set
// * the 'location' property in the simulated event on mobile devices, even when iOS neglects to
// * set it for real events.
// */
// return runKeyboardTestFromJSON('/engine_tests/chirality.json',
// {usingOSK: false},
// assert.equal,
// testconfig.timeouts.scriptLoad).then(() => {
// /* We only really care to test the 'desktop' OSK because of how it directly models the modifier keys.
// *
// * The 'phone' and 'layout' versions take shortcuts that bypass any tricky chiral logic;
// * a better test for those would be to ensure the touch OSK is constructed properly.
// */
// if(!testconfig.mobile) {
// return runKeyboardTestFromJSON('/engine_tests/chirality.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
// }
// });
// });
// });

View file

@ -1,113 +1,113 @@
var assert = chai.assert;
// var assert = chai.assert;
describe('Event Management', function() {
this.timeout(testconfig.timeouts.standard);
// describe('Event Management', function() {
// this.timeout(testconfig.timeouts.standard);
before(function() {
this.timeout(testconfig.timeouts.scriptLoad * 2);
fixture.setBase('fixtures');
fixture.load("eventTestConfig.html");
// before(function() {
// this.timeout(testconfig.timeouts.scriptLoad * 2);
// fixture.setBase('fixtures');
// fixture.load("eventTestConfig.html");
return setupKMW(null, testconfig.timeouts.scriptLoad).then(() => {
// We use this keyboard since we only need minimal input functionality for these tests.
// Smaller is better when dealing with net latency.
return loadKeyboardFromJSON("/keyboards/test_simple_deadkeys.json", testconfig.timeouts.scriptLoad);
});
});
// return setupKMW(null, testconfig.timeouts.scriptLoad).then(() => {
// // We use this keyboard since we only need minimal input functionality for these tests.
// // Smaller is better when dealing with net latency.
// return loadKeyboardFromJSON("/keyboards/test_simple_deadkeys.json", testconfig.timeouts.scriptLoad);
// });
// });
after(function() {
teardownKMW();
});
// after(function() {
// teardownKMW();
// });
it('Keystroke-based onChange event generation', function() {
var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0};
var event = new KMWRecorder.PhysicalInputEventSpec(simple_A);
// it('Keystroke-based onChange event generation', function() {
// var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0};
// var event = new KMWRecorder.PhysicalInputEventSpec(simple_A);
var ele = document.getElementById("input");
// var ele = document.getElementById("input");
ele.onchange = function() {
ele.onchange = null;
}
// ele.onchange = function() {
// ele.onchange = null;
// }
// A bit of a force-hack to ensure the element is seen as active for the tests.
com.keyman.dom['DOMEventHandlers'].states._lastActiveElement = ele;
com.keyman.dom['DOMEventHandlers'].states._activeElement = ele;
// // A bit of a force-hack to ensure the element is seen as active for the tests.
// com.keyman.dom['DOMEventHandlers'].states._lastActiveElement = ele;
// com.keyman.dom['DOMEventHandlers'].states._activeElement = ele;
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(event);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(event);
let focusEvent = new FocusEvent('blur', {relatedTarget: ele});
ele.dispatchEvent(focusEvent);
// let focusEvent = new FocusEvent('blur', {relatedTarget: ele});
// ele.dispatchEvent(focusEvent);
// Asserts that the handler is called. As the handler clears itself, it will only
// remain set if it hasn't been called.
assert.isNull(ele.onchange, '`onchange` handler was not called');
});
// // Asserts that the handler is called. As the handler clears itself, it will only
// // remain set if it hasn't been called.
// assert.isNull(ele.onchange, '`onchange` handler was not called');
// });
it('OSK-based onChange event generation', function() {
var simple_A = {"type":"osk","keyID":"default-K_A"};
var event = new KMWRecorder.OSKInputEventSpec(simple_A);
// it('OSK-based onChange event generation', function() {
// var simple_A = {"type":"osk","keyID":"default-K_A"};
// var event = new KMWRecorder.OSKInputEventSpec(simple_A);
var ele = document.getElementById("input");
// var ele = document.getElementById("input");
ele.onchange = function() {
ele.onchange = null;
}
// ele.onchange = function() {
// ele.onchange = null;
// }
// A bit of a force-hack to ensure the element is seen as active for the tests.
com.keyman.dom['DOMEventHandlers'].states._lastActiveElement = ele;
com.keyman.dom['DOMEventHandlers'].states._activeElement = ele;
// // A bit of a force-hack to ensure the element is seen as active for the tests.
// com.keyman.dom['DOMEventHandlers'].states._lastActiveElement = ele;
// com.keyman.dom['DOMEventHandlers'].states._activeElement = ele;
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(event);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(event);
let focusEvent = new FocusEvent('blur', {relatedTarget: ele});
ele.dispatchEvent(focusEvent);
// let focusEvent = new FocusEvent('blur', {relatedTarget: ele});
// ele.dispatchEvent(focusEvent);
// Asserts that the handler is called. As the handler clears itself, it will only
// remain set if it hasn't been called.
assert.isNull(ele.onchange, '`onchange` handler was not called');
});
// // Asserts that the handler is called. As the handler clears itself, it will only
// // remain set if it hasn't been called.
// assert.isNull(ele.onchange, '`onchange` handler was not called');
// });
it('Keystroke-based onInput event generation', function() {
var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0};
var event = new KMWRecorder.PhysicalInputEventSpec(simple_A);
// it('Keystroke-based onInput event generation', function() {
// var simple_A = {"type":"key","key":"a","code":"KeyA","keyCode":65,"modifierSet":0,"location":0};
// var event = new KMWRecorder.PhysicalInputEventSpec(simple_A);
var ele = document.getElementById("input");
// var ele = document.getElementById("input");
var counterObj = {i:0};
var fin = 3;
// var counterObj = {i:0};
// var fin = 3;
ele.addEventListener("input", function() {
counterObj.i++;
});
// ele.addEventListener("input", function() {
// counterObj.i++;
// });
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(event);
eventDriver.simulateEvent(event);
eventDriver.simulateEvent(event);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(event);
// eventDriver.simulateEvent(event);
// eventDriver.simulateEvent(event);
assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
});
// assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
// });
it('OSK-based onInput event generation', function() {
var simple_A = {"type":"osk","keyID":"default-K_A"};
var event = new KMWRecorder.OSKInputEventSpec(simple_A);
// it('OSK-based onInput event generation', function() {
// var simple_A = {"type":"osk","keyID":"default-K_A"};
// var event = new KMWRecorder.OSKInputEventSpec(simple_A);
var ele = document.getElementById("input");
// var ele = document.getElementById("input");
var counterObj = {i:0};
var fin = 3;
// var counterObj = {i:0};
// var fin = 3;
ele.addEventListener("input", function() {
counterObj.i++;
});
// ele.addEventListener("input", function() {
// counterObj.i++;
// });
let eventDriver = new KMWRecorder.BrowserDriver(ele);
eventDriver.simulateEvent(event);
eventDriver.simulateEvent(event);
eventDriver.simulateEvent(event);
// let eventDriver = new KMWRecorder.BrowserDriver(ele);
// eventDriver.simulateEvent(event);
// eventDriver.simulateEvent(event);
// eventDriver.simulateEvent(event);
assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
});
});
// assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
// });
// });

View file

@ -1,4 +1,12 @@
var assert = chai.assert;
let assert = chai.assert;
import { Mock } from '/@keymanapp/keyboard-processor/build/obj/text/outputTarget.js';
import { Input } from '/@keymanapp/keyman/build/engine/element-wrappers/lib/index.mjs';
import extendString from '../../../../../common/web/utils/build/obj/kmwstring.js';
import { toSupplementaryPairString, DynamicElements } from '../test_utils.js';
extendString();
var MockTests;
@ -26,7 +34,7 @@ if(typeof MockTests == 'undefined') {
MockTests.initBase = function() {
var id = DynamicElements.addInput();
var elem = document.getElementById(id);
var wrapper = new com.keyman.dom.targets.Input(elem);
var wrapper = new Input(elem);
return wrapper;
}
@ -86,7 +94,7 @@ describe('OutputTarget Mocking', function() {
describe('The "Mock" output target', function() {
describe('Initialization', function() {
it('properly initializes from a raw string', function() {
var mock = new com.keyman.text.Mock(MockTests.Apple.mixed);
var mock = new Mock(MockTests.Apple.mixed);
assert.equal(mock.getText(), MockTests.Apple.mixed);
assert.equal(mock.getDeadkeyCaret(), 5);
@ -95,7 +103,7 @@ describe('OutputTarget Mocking', function() {
it('copies an existing OutputTarget without a text selection', function() {
var base = MockTests.setupBase(4);
var mock = com.keyman.text.Mock.from(base);
var mock = Mock.from(base);
assert.equal(mock.getText(), MockTests.Apple.mixed);
assert.deepEqual(mock.deadkeys(), base.deadkeys());
});
@ -103,7 +111,7 @@ describe('OutputTarget Mocking', function() {
it('copies an existing OutputTarget with a text selection', function() {
var base = MockTests.setupBase(4, 5);
var mock = com.keyman.text.Mock.from(base);
var mock = Mock.from(base);
// The selection should appear to be automatically deleted, as any text mutation
// by KMW would automatically erase the text anyway.
assert.equal(mock.getText(), MockTests.Apple.mixed.substr(0, 5));
@ -117,7 +125,7 @@ describe('OutputTarget Mocking', function() {
it('is not affected by mutation of the source element', function() {
// Already-verified code
var base = MockTests.setupBase(4);
var mock = com.keyman.text.Mock.from(base);
var mock = Mock.from(base);
var baseInitDks = base.deadkeys().clone();
// Now for the actual test.
@ -136,7 +144,7 @@ describe('OutputTarget Mocking', function() {
it('does not affect the source element when mutated', function() {
// Already-verified code
var base = MockTests.setupBase(4);
var mock = com.keyman.text.Mock.from(base);
var mock = Mock.from(base);
var baseInitDks = base.deadkeys().clone();
// Now for the actual test.

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
module.exports = function(config) {
var base = require("./base.conf.js");
var base = require("./base.conf.cjs");
var specifics = {
// test results reporter to use

View file

@ -2,6 +2,8 @@
// We'll let things break after this reports, since this will likely signal a LOT of other failures.
var assert = chai.assert;
import Device from '/@keymanapp/keyman/build/engine/device-detect/lib/index.mjs';
/* Note - we still have to prevent errors setting up test resources;
* Karma will fail to report errors for affected browsers otherwise.
*
@ -16,7 +18,7 @@ describe('Test Initialization', function() {
it("Detects device without JS errors", function() {
var device;
try {
device = new com.keyman.Device();
device = new Device();
device.detect();
console.log("Detected platform: " + device.browser + " on " + device.OS + " with form factor " + device.formFactor);

View file

@ -1,200 +1,199 @@
// KeymanWeb test suite - processing of the Karma configuration's client.args parameter.
// // KeymanWeb test suite - processing of the Karma configuration's client.args parameter.
com = com || {};
com.keyman = com.keyman || {};
com.keyman.karma = com.keyman.karma || {};
com.keyman.karma.DEVICE_DETECT_FAILURE = false;
import Device from '/@keymanapp/keyman/build/engine/device-detect/lib/index.mjs';
export let DEVICE_DETECT_FAILURE = false;
// If we've set things up to support Device dection without loading KMW...
try {
let device = new com.keyman.Device();
let device = new Device();
device.detect();
} catch (err) {
// Sets a warning flag that unit-test files can use to disable themselves.
com.keyman.karma.DEVICE_DETECT_FAILURE = true;
DEVICE_DETECT_FAILURE = true;
}
// Keyman test suite utility methods
// // Keyman test suite utility methods
var setupKMW = function(kmwOptions, timeout) {
var ui;
// var setupKMW = function(kmwOptions, timeout) {
// var ui;
if(typeof(kmwOptions) == 'string' || typeof(kmwOptions) == 'undefined' || kmwOptions == null) {
ui = kmwOptions;
// if(typeof(kmwOptions) == 'string' || typeof(kmwOptions) == 'undefined' || kmwOptions == null) {
// ui = kmwOptions;
var kmwOptions = {
attachType:'auto',
root:'source',
resources:'../../../../source'
};
// var kmwOptions = {
// attachType:'auto',
// root:'source',
// resources:'../../../../source'
// };
if(ui) {
kmwOptions.ui = ui;
}
}
// if(ui) {
// kmwOptions.ui = ui;
// }
// }
const kmwPromise = setupScript('source/keymanweb.js', timeout, (scriptEle) => {
fixture.el.appendChild(scriptEle);
});
// const kmwPromise = setupScript('source/keymanweb.js', timeout, (scriptEle) => {
// fixture.el.appendChild(scriptEle);
// });
ui = kmwOptions.ui;
// ui = kmwOptions.ui;
kmwOptions.attachType = kmwOptions.attachType ? kmwOptions.attachType : 'auto';
// kmwOptions.attachType = kmwOptions.attachType ? kmwOptions.attachType : 'auto';
if(!kmwOptions.root) {
kmwOptions.root = 'source';
}
// if(!kmwOptions.root) {
// kmwOptions.root = 'source';
// }
if(!kmwOptions.resources) {
kmwOptions.resources = '../../../../source';
}
// if(!kmwOptions.resources) {
// kmwOptions.resources = '../../../../source';
// }
let uiPromise;
if(ui) {
uiPromise = setupScript('ui-source/kmwui' + ui + '.js', timeout, (scriptEle) => {
fixture.el.appendChild(scriptEle);
});
// let uiPromise;
// if(ui) {
// uiPromise = setupScript('ui-source/kmwui' + ui + '.js', timeout, (scriptEle) => {
// fixture.el.appendChild(scriptEle);
// });
kmwOptions.ui=ui;
}
// kmwOptions.ui=ui;
// }
let compositePromise = kmwPromise;
if(uiPromise) {
compositePromise = Promise.all([kmwPromise, uiPromise]);
}
// let compositePromise = kmwPromise;
// if(uiPromise) {
// compositePromise = Promise.all([kmwPromise, uiPromise]);
// }
return finalPromise = compositePromise.then(() => {
if(window['keyman']) {
return window['keyman'].init(kmwOptions);
} else {
return Promise.reject();
}
});
}
// return finalPromise = compositePromise.then(() => {
// if(window['keyman']) {
// return window['keyman'].init(kmwOptions);
// } else {
// return Promise.reject();
// }
// });
// }
/**
* Produces a script element tied to a Promise for its eventual load (or failure thereof).
*
* The script element is only available via callback due to implementation constraints.
*
* @param {*} src The source script's (relative) path on the test server.
* @param {*} timeout
* @param {*} functor A callback to handle the script element.
* @returns
*/
var setupScript = function(src, timeout, functor) {
return new Promise((resolve, reject) => {
const Lscript = document.createElement('script');
let hasResolved = false;
Lscript.charset="UTF-8"; // KMEW-89
Lscript.type = 'text/javascript';
Lscript.async = false;
// /**
// * Produces a script element tied to a Promise for its eventual load (or failure thereof).
// *
// * The script element is only available via callback due to implementation constraints.
// *
// * @param {*} src The source script's (relative) path on the test server.
// * @param {*} timeout
// * @param {*} functor A callback to handle the script element.
// * @returns
// */
// var setupScript = function(src, timeout, functor) {
// return new Promise((resolve, reject) => {
// const Lscript = document.createElement('script');
// let hasResolved = false;
// Lscript.charset="UTF-8"; // KMEW-89
// Lscript.type = 'text/javascript';
// Lscript.async = false;
const timer = window.setTimeout(() => {
reject("Script load attempt timed out.");
}, timeout);
// const timer = window.setTimeout(() => {
// reject("Script load attempt timed out.");
// }, timeout);
Lscript.onload = Lscript.onreadystatechange = () => {
window.clearTimeout(timer);
if(!hasResolved && (Lscript.readyState === undefined || Lscript.readyState == "complete")) {
hasResolved = true;
resolve();
}
}
// Lscript.onload = Lscript.onreadystatechange = () => {
// window.clearTimeout(timer);
// if(!hasResolved && (Lscript.readyState === undefined || Lscript.readyState == "complete")) {
// hasResolved = true;
// resolve();
// }
// }
Lscript.onerror = (err) => {
window.clearTimeout(timer);
reject(err);
}
// Lscript.onerror = (err) => {
// window.clearTimeout(timer);
// reject(err);
// }
Lscript.src = src;
// Lscript.src = src;
functor(Lscript);
});
}
// functor(Lscript);
// });
// }
var teardownKMW = function() {
var error = null;
if(keyman) { // If our setupKMW fails somehow, this guard prevents a second error report on teardown.
// var teardownKMW = function() {
// var error = null;
// if(keyman) { // If our setupKMW fails somehow, this guard prevents a second error report on teardown.
// We want to be SURE teardown works correctly, or we'll get lots of strange errors on other tests.
// Thus, error-handling on shutdown itself. It HAS mattered.
try {
keyman['shutdown']();
} catch(err) {
error = err;
}
// // We want to be SURE teardown works correctly, or we'll get lots of strange errors on other tests.
// // Thus, error-handling on shutdown itself. It HAS mattered.
// try {
// keyman['shutdown']();
// } catch(err) {
// error = err;
// }
try {
var success = delete window["keyman"];
if(!success) {
window["keyman"] = undefined;
}
} finally {
if(error) {
console.log("Error during KMW shutdown!");
throw error;
}
}
}
}
// try {
// var success = delete window["keyman"];
// if(!success) {
// window["keyman"] = undefined;
// }
// } finally {
// if(error) {
// console.log("Error during KMW shutdown!");
// throw error;
// }
// }
// }
// }
var loadKeyboardStub = function(stub, timeout, params) {
var kbdName = "Keyboard_" + stub.id;
// var loadKeyboardStub = function(stub, timeout, params) {
// var kbdName = "Keyboard_" + stub.id;
keyman.addKeyboards(stub);
if(!params || !params.passive) {
return keyman.setActiveKeyboard(kbdName, stub.languages.id);
} else if(keyman.getActiveKeyboard() != kbdName) {
return setupScript(stub.filename, timeout, (ele) => {
fixture.el.appendChild(ele);
});
} else {
return Promise.resolve();
}
}
// keyman.addKeyboards(stub);
// if(!params || !params.passive) {
// return keyman.setActiveKeyboard(kbdName, stub.languages.id);
// } else if(keyman.getActiveKeyboard() != kbdName) {
// return setupScript(stub.filename, timeout, (ele) => {
// fixture.el.appendChild(ele);
// });
// } else {
// return Promise.resolve();
// }
// }
var loadKeyboardFromJSON = function(jsonPath, timeout, params) {
var stub = fixture.load(jsonPath, true);
// var loadKeyboardFromJSON = function(jsonPath, timeout, params) {
// var stub = fixture.load(jsonPath, true);
return loadKeyboardStub(stub, timeout, params);
}
// return loadKeyboardStub(stub, timeout, params);
// }
function runLoadedKeyboardTest(testDef, device, usingOSK, assertCallback) {
var inputElem = document.getElementById('singleton');
// function runLoadedKeyboardTest(testDef, device, usingOSK, assertCallback) {
// var inputElem = document.getElementById('singleton');
let proctor = new KMWRecorder.BrowserProctor(inputElem, device, usingOSK, assertCallback);
testDef.test(proctor);
}
// let proctor = new KMWRecorder.BrowserProctor(inputElem, device, usingOSK, assertCallback);
// testDef.test(proctor);
// }
function runKeyboardTestFromJSON(jsonPath, params, assertCallback, timeout) {
var testSpec = new KMWRecorder.KeyboardTest(fixture.load(jsonPath, true));
let device = new com.keyman.Device();
device.detect();
// function runKeyboardTestFromJSON(jsonPath, params, assertCallback, timeout) {
// var testSpec = new KMWRecorder.KeyboardTest(fixture.load(jsonPath, true));
// let device = new com.keyman.Device();
// device.detect();
return loadKeyboardStub(testSpec.keyboard, timeout).then(() => {
runLoadedKeyboardTest(testSpec, device.coreSpec, params.usingOSK, assertCallback);
}).finally(() => {
keyman.removeKeyboards(testSpec.keyboard.id);
});
}
// return loadKeyboardStub(testSpec.keyboard, timeout).then(() => {
// runLoadedKeyboardTest(testSpec, device.coreSpec, params.usingOSK, assertCallback);
// }).finally(() => {
// keyman.removeKeyboards(testSpec.keyboard.id);
// });
// }
function retrieveAndReset(Pelem) {
let val = Pelem.value;
Pelem.value = "";
// function retrieveAndReset(Pelem) {
// let val = Pelem.value;
// Pelem.value = "";
return val;
}
// return val;
// }
// Useful for tests related to strings with supplementary pairs.
var toSupplementaryPairString = function(code){
export function toSupplementaryPairString(code) {
var H = Math.floor((code - 0x10000) / 0x400) + 0xD800;
var L = (code - 0x10000) % 0x400 + 0xDC00;
return String.fromCharCode(H, L);
}
var toEscapedSupplementaryPairString = function(code){
export function toEscapedSupplementaryPairString(code) {
var H = (Math.floor((code - 0x10000) / 0x400) + 0xD800).toString(16);
var L = ((code - 0x10000) % 0x400 + 0xDC00).toString(16);
@ -204,7 +203,7 @@ var toEscapedSupplementaryPairString = function(code){
// Defines an object for dynamically adding elements for testing purposes.
// Designed for use with the robustAttachment.html fixture.
var DynamicElements;
export let DynamicElements;
var inputCounter = 0;
if(typeof(DynamicElements) == 'undefined') {
@ -316,17 +315,17 @@ if(typeof(DynamicElements) == 'undefined') {
}
}
// Is utilized only by the attachmentAPI test case, but it was originally defined as part of the same
// object as the rest of DynamicElements, which is useful for numerous test cases.
DynamicElements.init = function() {
var s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0};
DynamicElements.keyCommand = new KMWRecorder.PhysicalInputEventSpec(s_key_json);
// // Is utilized only by the attachmentAPI test case, but it was originally defined as part of the same
// // object as the rest of DynamicElements, which is useful for numerous test cases.
// DynamicElements.init = function() {
// var s_key_json = {"type": "key", "key":"s", "code":"KeyS","keyCode":83,"modifierSet":0,"location":0};
// DynamicElements.keyCommand = new KMWRecorder.PhysicalInputEventSpec(s_key_json);
DynamicElements.enabledLaoOutput = "ຫ";
DynamicElements.enabledKhmerOutput = "ស";
// Simulated JavaScript events do not produce text output.
DynamicElements.disabledOutput = "";
}
// DynamicElements.enabledLaoOutput = "ຫ";
// DynamicElements.enabledKhmerOutput = "ស";
// // Simulated JavaScript events do not produce text output.
// DynamicElements.disabledOutput = "";
// }
DynamicElements.init();
}
// DynamicElements.init();
}