diff --git a/common/web/recorder/build.sh b/common/web/recorder/build.sh index fb566a18c8..3bee5dc024 100755 --- a/common/web/recorder/build.sh +++ b/common/web/recorder/build.sh @@ -22,16 +22,11 @@ builder_describe \ "@../keyman-version" \ configure \ clean \ - build \ - ":module Builds recorder-core module" \ - ":proctor Builds headless-testing, node-oriented 'proctor' component" + build builder_describe_outputs \ - configure "/node_modules" \ - configure:module "/node_modules" \ - configure:proctor "/node_modules" \ - build:module "build/index.js" \ - build:proctor "build/nodeProctor/index.js" + configure "/node_modules" \ + build "build/index.js" builder_parse "$@" @@ -40,22 +35,12 @@ if builder_start_action configure; then builder_finish_action success configure fi -if builder_start_action clean:module; then - npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/src/tsconfig.json" - builder_finish_action success clean:module +if builder_start_action clean; then + npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/tsconfig.json" + builder_finish_action success clean fi -if builder_start_action clean:proctor; then - npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json" - builder_finish_action success clean:proctor -fi - -if builder_start_action build:module; then - npm run tsc -- --build "$THIS_SCRIPT_PATH/src/tsconfig.json" - builder_finish_action success build:module -fi - -if builder_start_action build:proctor; then - npm run tsc -- --build "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json" - builder_finish_action success build:proctor +if builder_start_action build; then + npm run tsc -- --build "$THIS_SCRIPT_PATH/tsconfig.json" + builder_finish_action success build fi \ No newline at end of file diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index d01dcad71b..f845891df9 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,774 +1,780 @@ /// -namespace KMWRecorder { - //#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation - export abstract class InputEventSpec { - type: "key" | "osk"; - static fromJSONObject(obj: any): InputEventSpec { - if(obj && obj.type) { - if(obj.type == "key") { - return new PhysicalInputEventSpec(obj); - } else if(obj.type == "osk") { - return new OSKInputEventSpec(obj); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); - } - } +import KeyEvent, { KeyDistribution } from "keyboard-processor/build/modules/text/keyEvent.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; +import Proctor from "./proctor.js"; + +import * as utils from "utils/build/modules/index.js"; + +//#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation +export abstract class InputEventSpec { + type: "key" | "osk"; + static fromJSONObject(obj: any): InputEventSpec { + if(obj && obj.type) { + if(obj.type == "key") { + return new PhysicalInputEventSpec(obj); + } else if(obj.type == "osk") { + return new OSKInputEventSpec(obj); + } + } else { + throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); } } - export class PhysicalInputEventSpec extends InputEventSpec { - static readonly modifierCodes: { [mod:string]: number } = { - "Shift":0x0001, - "Control":0x0002, - "Alt":0x0004, - "Meta":0x0008, - "CapsLock":0x0010, - "NumLock":0x0020, - "ScrollLock":0x0040 - }; + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; + } +} - // KeyboardEvent properties - type: "key" = "key"; - key: string; - code: string; - keyCode: number; - modifierSet: number; - location: number; +export class PhysicalInputEventSpec extends InputEventSpec { + static readonly modifierCodes: { [mod:string]: number } = { + "Shift":0x0001, + "Control":0x0002, + "Alt":0x0004, + "Meta":0x0008, + "CapsLock":0x0010, + "NumLock":0x0020, + "ScrollLock":0x0040 + }; - constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. - super(); + // KeyboardEvent properties + type: "key" = "key"; + key: string; + code: string; + keyCode: number; + modifierSet: number; + location: number; - if(e) { - this.key = e.key; - this.code = e.code; - this.keyCode = e.keyCode; - this.modifierSet = e.modifierSet; - this.location = e.location; - } - } + constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. + super(); - getModifierState(key: string): boolean { - return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; - } - - generateModifierString(): string { - var list: string = ""; - - for(var key in PhysicalInputEventSpec.modifierCodes) { - if(this.getModifierState(key)) { - list += ((list != "" ? " " : "") + key); - } - } - - return list; + if(e) { + this.key = e.key; + this.code = e.code; + this.keyCode = e.keyCode; + this.modifierSet = e.modifierSet; + this.location = e.location; } } - export class OSKInputEventSpec extends InputEventSpec { - type: "osk" = "osk"; - keyID: string; - - // The parameter may be used to reconstruct the item from raw JSON. - constructor(e?: OSKInputEventSpec) { - super(); - if(e) { - this.keyID = e.keyID; - } - } - } - //#endregion - - export abstract class RecordedKeystroke { - type: "key" | "osk"; - - static fromJSONObject(obj: any): RecordedKeystroke { - if(obj && obj.type) { - if(obj.type == "key") { - return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); - } else if(obj && obj.type) { - return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); - } - } - - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; - } - - /** - * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. - */ - abstract get inputEventSpec(): InputEventSpec; + getModifierState(key: string): boolean { + return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; } - export class RecordedPhysicalKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "key" = "key"; + generateModifierString(): string { + var list: string = ""; - keyCode: number; // may be different from eventSpec's value b/c keymapping - states: number; - modifiers: number; - modifierChanged: boolean; - isVirtualKey: boolean; - vkCode: number; // may be possible to eliminate; differences arise from mnemonics. - - eventSpec: PhysicalInputEventSpec; - - constructor(keystroke: RecordedPhysicalKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent, eventSpec: PhysicalInputEventSpec) - constructor(keystroke: RecordedPhysicalKeystroke|com.keyman.text.KeyEvent, eventSpec?: PhysicalInputEventSpec) { - super(); - - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - // Store what is necessary for headless event reconstruction. - keystroke = keystroke as com.keyman.text.KeyEvent; - this.keyCode = keystroke.Lcode; - this.states = keystroke.Lstates; - this.modifiers = keystroke.Lmodifiers; - this.modifierChanged = !!keystroke.LmodifierChange; - this.isVirtualKey = keystroke.LisVirtualKey; - this.vkCode = keystroke.vkCode; - - // Also store the DOM-based event spec for use in integrated testing. - this.eventSpec = eventSpec; - } else { - // It might be a raw object, from JSON. - this.keyCode = keystroke.keyCode; - this.states = keystroke.states; - this.modifiers = keystroke.modifiers; - this.modifierChanged = keystroke.modifierChanged; - this.isVirtualKey = keystroke.isVirtualKey; - this.vkCode = keystroke.vkCode; - - this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. + for(var key in PhysicalInputEventSpec.modifierCodes) { + if(this.getModifierState(key)) { + list += ((list != "" ? " " : "") + key); } } - get inputEventSpec(): InputEventSpec { - return this.eventSpec; + return list; + } +} + +export class OSKInputEventSpec extends InputEventSpec { + type: "osk" = "osk"; + keyID: string; + + // The parameter may be used to reconstruct the item from raw JSON. + constructor(e?: OSKInputEventSpec) { + super(); + if(e) { + this.keyID = e.keyID; + } + } +} +//#endregion + +export abstract class RecordedKeystroke { + type: "key" | "osk"; + + static fromJSONObject(obj: any): RecordedKeystroke { + if(obj && obj.type) { + if(obj.type == "key") { + return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); + } else if(obj && obj.type) { + return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); + } + } else { + throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); } } - export class RecordedSyntheticKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "osk" = "osk"; - - keyName: string; - layer: string; - - keyDistribution?: com.keyman.text.KeyDistribution; - - constructor(keystroke: RecordedSyntheticKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent) - constructor(keystroke: RecordedSyntheticKeystroke|com.keyman.text.KeyEvent) { - super(); - - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - keystroke = keystroke as com.keyman.text.KeyEvent; - // Store what is necessary for headless event reconstruction. - - // Also store the DOM-based event spec for use in integrated testing. - this.layer = keystroke.kbdLayer; - this.keyName = keystroke.kName; - this.keyDistribution = keystroke.keyDistribution; - } else { - // It might be a raw object, from JSON. - this.layer = keystroke.layer; - this.keyName = keystroke.keyName; - this.keyDistribution = keystroke.keyDistribution; - } - } - - get inputEventSpec(): InputEventSpec { - let eventSpec = new OSKInputEventSpec(); - eventSpec.keyID = this.layer + '-' + this.keyName; - - return eventSpec; - } - } - - export abstract class TestSequence { - inputs: KeyRecord[]; - output: string; - msg?: string; - - abstract hasOSKInteraction(): boolean; - - test(proctor: Proctor, target?: com.keyman.text.OutputTarget): {success: boolean, result: string} { - // Start with an empty OutputTarget and a fresh KeyboardProcessor. - if(!target) { - target = new com.keyman.text.Mock(); - } - - proctor.before(); - - let result = proctor.simulateSequence(this, target); - proctor.assertEquals(result, this.output, this.msg); - - return {success: (result == this.output), result: result}; - } - - toPrettyJSON(): string { - var str = "{ "; - if(this.output) { - str += "\"output\": \"" + this.output + "\", "; - } - str += "\"inputs\": [\n"; - for(var i = 0; i < this.inputs.length; i++) { - str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); - } - if(this.msg) { - str += "], \"message\": \"" + this.msg + "\" }"; - } else { - str += "]}"; - } - return str; - } - } - - export class InputEventSpecSequence extends TestSequence { - inputs: InputEventSpec[]; - output: string; - msg?: string; - - constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { - super(); - - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; - - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); - } - - this.output = ins.output; - this.msg = ins.msg; - return; - } - } else { - this.inputs = []; - } - - if(outs) { - this.output = outs; - } - - if(msg) { - this.msg = msg; - } - } - - addInput(event: InputEventSpec, output: string) { - this.inputs.push(event); - this.output = output; - } - - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof OSKInputEventSpec) { - return true; - } - } - - return false; - } - } - - export class RecordedKeystrokeSequence extends TestSequence { - inputs: RecordedKeystroke[]; - output: string; - msg?: string; - - constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) - constructor(sequence: RecordedKeystrokeSequence) - constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { - super(); - - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; - - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); - } - - this.output = ins.output; - this.msg = ins.msg; - return; - } - } else { - this.inputs = []; - } - - if(outs) { - this.output = outs; - } - - if(msg) { - this.msg = msg; - } - } - - addInput(event: RecordedKeystroke, output: string) { - this.inputs.push(event); - this.output = output; - } - - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { - return true; - } - } - - return false; - } - } - - class FontStubForLanguage { - family: string; - source: string[]; - - constructor(activeStubEntry: any) { - this.family = activeStubEntry.family; - - var src = activeStubEntry.files; - if(!(src instanceof Array)) { - src = [ src ]; - } - - this.source = []; - for(var i=0; i < src.length; i++) { - this.source.push(activeStubEntry.path + src[i]); - } - } - } - - export class LanguageStubForKeyboard { - id: string; - name: string; - region: string; - font?: FontStubForLanguage; - oskFont?: FontStubForLanguage; - - constructor(activeStub: any) { - if(activeStub.KLC) { - this.id = activeStub.KLC; - this.name = activeStub.KL; - this.region = activeStub.KR; - - // Fonts. - if(activeStub.KFont) { - this.font = new FontStubForLanguage(activeStub.KFont); - } - if(activeStub.KOskFont) { - this.oskFont = new FontStubForLanguage(activeStub.KOskFont); - } - } else { - this.id = activeStub.id; - this.name = activeStub.name; - this.region = activeStub.region; - - // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. - this.font = activeStub.font; - this.oskFont = activeStub.oskFont; - } - } - } - - export class KeyboardStub { - id: string; - name: string; - filename: string; - languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; - - // Constructs a stub usable with KeymanWeb's addKeyboards() API function from - // the internally-tracked ActiveStub value for that keyboard. - constructor(json?: KeyboardStub) { - if(json) { - this.id = json.id; - this.name = json.name; - this.filename = json.filename; - - if(!Array.isArray(json.languages)) { - this.languages = new LanguageStubForKeyboard(json.languages); - } else { - this.languages = []; - for(var i=0; i < json.languages.length; i++) { - this.languages.push(new LanguageStubForKeyboard(json.languages[i])); - } - } - } - } - - getFirstLanguage() { - if(this.languages instanceof LanguageStubForKeyboard) { - return this.languages.id; - } else { - return this.languages[0].id; - } - } - } - - type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; - type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; - type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! - - export class Constraint { - target: TARGET; - validOSList?: OS[]; - validBrowsers?: BROWSER[]; - - constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { - if(typeof(target) == 'string') { - this.target = target; - this.validOSList = validOSList; - this.validBrowsers = validBrowsers; - } else { - var json = target; - this.target = json.target; - this.validOSList = json.validOSList; - this.validBrowsers = json.validBrowsers; - } - } - - matchesClient(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - // #1: Platform check. - if(usingOSK === true) { - if(this.target != device.formFactor) { - return false; - } - } else if(usingOSK === false) { - if(this.target != 'hardware') { - return false; - } - } else if(this.target != device.formFactor && this.target != 'hardware') { - return false; - } - - if(this.validOSList) { - if(this.validOSList.indexOf(device.OS as OS) == -1) { - return false; - } - } - - if(this.validBrowsers) { - if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { - return false; - } - } - - return true; - } - - // Checks if another Constraint instance is functionally identical to this one. - equals(other: Constraint) { - if(this.target != other.target) { - return false; - } - - var list1 = this.validOSList ? this.validOSList : ['any']; - var list2 = other.validOSList ? other.validOSList : ['any']; - - if(list1.sort().join(',') != list2.sort().join(',')) { - return false; - } - - list1 = this.validBrowsers ? this.validBrowsers : ['web']; - list2 = other.validBrowsers ? other.validBrowsers : ['web']; - - if(list1.sort().join(',') != list2.sort().join(',')) { - return false; - } - - return true; - } - } - - export class TestFailure { - constraint: Constraint; - test: InputEventSpecSequence; - result: string; - - constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { - this.constraint = constraint; - this.test = test; - this.result = output; - } - } - - export interface TestSet> { - constraint: Constraint; - - addTest(seq: Sequence): void; - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean): boolean; - test(proctor: Proctor): TestFailure[]; + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; } /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. */ - export class EventSpecTestSet implements TestSet { - constraint: Constraint; - testSet: InputEventSpecSequence[]; + abstract get inputEventSpec(): InputEventSpec; +} - constructor(constraint: Constraint|EventSpecTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as EventSpecTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; +export class RecordedPhysicalKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "key" = "key"; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new InputEventSpecSequence(json.testSet[i])); - } - } - } + keyCode: number; // may be different from eventSpec's value b/c keymapping + states: number; + modifiers: number; + modifierChanged: boolean; + isVirtualKey: boolean; + vkCode: number; // may be possible to eliminate; differences arise from mnemonics. - addTest(seq: InputEventSpecSequence) { - this.testSet.push(seq); - } + eventSpec: PhysicalInputEventSpec; - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } + constructor(keystroke: RecordedPhysicalKeystroke) + constructor(keystroke: KeyEvent, eventSpec: PhysicalInputEventSpec) + constructor(keystroke: RecordedPhysicalKeystroke|KeyEvent, eventSpec?: PhysicalInputEventSpec) { + super(); - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + // Store what is necessary for headless event reconstruction. + keystroke = keystroke as KeyEvent; + this.keyCode = keystroke.Lcode; + this.states = keystroke.Lstates; + this.modifiers = keystroke.Lmodifiers; + this.modifierChanged = !!keystroke.LmodifierChange; + this.isVirtualKey = keystroke.LisVirtualKey; + this.vkCode = keystroke.vkCode; - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } - } + // Also store the DOM-based event spec for use in integrated testing. + this.eventSpec = eventSpec; + } else { + // It might be a raw object, from JSON. + this.keyCode = keystroke.keyCode; + this.states = keystroke.states; + this.modifiers = keystroke.modifiers; + this.modifierChanged = keystroke.modifierChanged; + this.isVirtualKey = keystroke.isVirtualKey; + this.vkCode = keystroke.vkCode; - return failures.length > 0 ? failures : null; + this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. } } + get inputEventSpec(): InputEventSpec { + return this.eventSpec; + } +} + +export class RecordedSyntheticKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "osk" = "osk"; + + keyName: string; + layer: string; + + keyDistribution?: KeyDistribution; + + constructor(keystroke: RecordedSyntheticKeystroke) + constructor(keystroke: KeyEvent) + constructor(keystroke: RecordedSyntheticKeystroke|KeyEvent) { + super(); + + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + keystroke = keystroke as KeyEvent; + // Store what is necessary for headless event reconstruction. + + // Also store the DOM-based event spec for use in integrated testing. + this.layer = keystroke.kbdLayer; + this.keyName = keystroke.kName; + this.keyDistribution = keystroke.keyDistribution; + } else { + // It might be a raw object, from JSON. + this.layer = keystroke.layer; + this.keyName = keystroke.keyName; + this.keyDistribution = keystroke.keyDistribution; + } + } + + get inputEventSpec(): InputEventSpec { + let eventSpec = new OSKInputEventSpec(); + eventSpec.keyID = this.layer + '-' + this.keyName; + + return eventSpec; + } +} + +export abstract class TestSequence { + inputs: KeyRecord[]; + output: string; + msg?: string; + + abstract hasOSKInteraction(): boolean; + + test(proctor: Proctor, target?: OutputTarget): {success: boolean, result: string} { + // Start with an empty OutputTarget and a fresh KeyboardProcessor. + if(!target) { + target = new Mock(); + } + + proctor.before(); + + let result = proctor.simulateSequence(this, target); + proctor.assertEquals(result, this.output, this.msg); + + return {success: (result == this.output), result: result}; + } + + toPrettyJSON(): string { + var str = "{ "; + if(this.output) { + str += "\"output\": \"" + this.output + "\", "; + } + str += "\"inputs\": [\n"; + for(var i = 0; i < this.inputs.length; i++) { + str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); + } + if(this.msg) { + str += "], \"message\": \"" + this.msg + "\" }"; + } else { + str += "]}"; + } + return str; + } +} + +export class InputEventSpecSequence extends TestSequence { + inputs: InputEventSpec[]; + output: string; + msg?: string; + + constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { + super(); + + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); + } else { + // We're constructing from existing JSON. + this.inputs = []; + + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); + } + + this.output = ins.output; + this.msg = ins.msg; + return; + } + } else { + this.inputs = []; + } + + if(outs) { + this.output = outs; + } + + if(msg) { + this.msg = msg; + } + } + + addInput(event: InputEventSpec, output: string) { + this.inputs.push(event); + this.output = output; + } + + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof OSKInputEventSpec) { + return true; + } + } + + return false; + } +} + +export class RecordedKeystrokeSequence extends TestSequence { + inputs: RecordedKeystroke[]; + output: string; + msg?: string; + + constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) + constructor(sequence: RecordedKeystrokeSequence) + constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { + super(); + + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); + } else { + // We're constructing from existing JSON. + this.inputs = []; + + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); + } + + this.output = ins.output; + this.msg = ins.msg; + return; + } + } else { + this.inputs = []; + } + + if(outs) { + this.output = outs; + } + + if(msg) { + this.msg = msg; + } + } + + addInput(event: RecordedKeystroke, output: string) { + this.inputs.push(event); + this.output = output; + } + + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { + return true; + } + } + + return false; + } +} + +class FontStubForLanguage { + family: string; + source: string[]; + + constructor(activeStubEntry: any) { + this.family = activeStubEntry.family; + + var src = activeStubEntry.files; + if(!(src instanceof Array)) { + src = [ src ]; + } + + this.source = []; + for(var i=0; i < src.length; i++) { + this.source.push(activeStubEntry.path + src[i]); + } + } +} + +export class LanguageStubForKeyboard { + id: string; + name: string; + region: string; + font?: FontStubForLanguage; + oskFont?: FontStubForLanguage; + + constructor(activeStub: any) { + if(activeStub.KLC) { + this.id = activeStub.KLC; + this.name = activeStub.KL; + this.region = activeStub.KR; + + // Fonts. + if(activeStub.KFont) { + this.font = new FontStubForLanguage(activeStub.KFont); + } + if(activeStub.KOskFont) { + this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + } + } else { + this.id = activeStub.id; + this.name = activeStub.name; + this.region = activeStub.region; + + // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. + this.font = activeStub.font; + this.oskFont = activeStub.oskFont; + } + } +} + +export class KeyboardStub { + id: string; + name: string; + filename: string; + languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; + + // Constructs a stub usable with KeymanWeb's addKeyboards() API function from + // the internally-tracked ActiveStub value for that keyboard. + constructor(json?: KeyboardStub) { + if(json) { + this.id = json.id; + this.name = json.name; + this.filename = json.filename; + + if(!Array.isArray(json.languages)) { + this.languages = new LanguageStubForKeyboard(json.languages); + } else { + this.languages = []; + for(var i=0; i < json.languages.length; i++) { + this.languages.push(new LanguageStubForKeyboard(json.languages[i])); + } + } + } + } + + getFirstLanguage() { + if(this.languages instanceof LanguageStubForKeyboard) { + return this.languages.id; + } else { + return this.languages[0].id; + } + } +} + +type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; +type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; +type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! + +export class Constraint { + target: TARGET; + validOSList?: OS[]; + validBrowsers?: BROWSER[]; + + constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { + if(typeof(target) == 'string') { + this.target = target; + this.validOSList = validOSList; + this.validBrowsers = validBrowsers; + } else { + var json = target; + this.target = json.target; + this.validOSList = json.validOSList; + this.validBrowsers = json.validBrowsers; + } + } + + matchesClient(device: utils.DeviceSpec, usingOSK?: boolean) { + // #1: Platform check. + if(usingOSK === true) { + if(this.target != device.formFactor) { + return false; + } + } else if(usingOSK === false) { + if(this.target != 'hardware') { + return false; + } + } else if(this.target != device.formFactor && this.target != 'hardware') { + return false; + } + + if(this.validOSList) { + if(this.validOSList.indexOf(device.OS as OS) == -1) { + return false; + } + } + + if(this.validBrowsers) { + if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { + return false; + } + } + + return true; + } + + // Checks if another Constraint instance is functionally identical to this one. + equals(other: Constraint) { + if(this.target != other.target) { + return false; + } + + var list1 = this.validOSList ? this.validOSList : ['any']; + var list2 = other.validOSList ? other.validOSList : ['any']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + list1 = this.validBrowsers ? this.validBrowsers : ['web']; + list2 = other.validBrowsers ? other.validBrowsers : ['web']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + return true; + } +} + +export class TestFailure { + constraint: Constraint; + test: InputEventSpecSequence; + result: string; + + constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { + this.constraint = constraint; + this.test = test; + this.result = output; + } +} + +export interface TestSet> { + constraint: Constraint; + + addTest(seq: Sequence): void; + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean): boolean; + test(proctor: Proctor): TestFailure[]; +} + +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class EventSpecTestSet implements TestSet { + constraint: Constraint; + testSet: InputEventSpecSequence[]; + + constructor(constraint: Constraint|EventSpecTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as EventSpecTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new InputEventSpecSequence(json.testSet[i])); + } + } + } + + addTest(seq: InputEventSpecSequence) { + this.testSet.push(seq); + } + + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } + + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; + + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); + } + } + + return failures.length > 0 ? failures : null; + } +} + +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class RecordedSequenceTestSet implements TestSet { + constraint: Constraint; + testSet: RecordedKeystrokeSequence[]; + + constructor(constraint: Constraint|RecordedSequenceTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as RecordedSequenceTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); + } + } + } + + addTest(seq: RecordedKeystrokeSequence) { + this.testSet.push(seq); + } + + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } + + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; + + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); + } + } + + return failures.length > 0 ? failures : null; + } + + toTestName(): string { + let name = "constraint: for " + this.constraint.target; + + if(this.constraint.target == 'hardware') { + name += " keyboard"; + } else { + name += " OSK"; + } + if(this.constraint.validOSList) { + name += " on OS of " + JSON.stringify(this.constraint.validOSList); + } + if(this.constraint.validBrowsers) { + name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); + } + + return name; + } +} + +export class KeyboardTest { + /** + * Indicates what version of KMW's recorder the spec conforms to. + */ + public specVersion: utils.Version = KeyboardTest.CURRENT_VERSION; + /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with + * only backward-compatible changes and minor tweaks to conform to internal API shifts. */ - export class RecordedSequenceTestSet implements TestSet { - constraint: Constraint; - testSet: RecordedKeystrokeSequence[]; + public static readonly FALLBACK_VERSION = new utils.Version("10.0"); + public static readonly CURRENT_VERSION = new utils.Version("14.0"); - constructor(constraint: Constraint|RecordedSequenceTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as RecordedSequenceTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; + /** + * The stub information to be passed into keyman.addKeyboards() in order to run the test. + */ + keyboard: KeyboardStub; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); - } - } - } + /** + * The master array of test sets, each of which specifies constraints a client must fulfill for + * the tests contained therein to be valid. + */ + inputTestSets: TestSet[]; - addTest(seq: RecordedKeystrokeSequence) { - this.testSet.push(seq); - } - - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } - - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; - - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } - } - - return failures.length > 0 ? failures : null; - } - - toTestName(): string { - let name = "constraint: for " + this.constraint.target; - - if(this.constraint.target == 'hardware') { - name += " keyboard"; - } else { - name += " OSK"; - } - if(this.constraint.validOSList) { - name += " on OS of " + JSON.stringify(this.constraint.validOSList); - } - if(this.constraint.validBrowsers) { - name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); - } - - return name; - } - } - - export class KeyboardTest { - /** - * Indicates what version of KMW's recorder the spec conforms to. - */ - public specVersion: com.keyman.utils.Version = KeyboardTest.CURRENT_VERSION; - - /** - * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with - * only backward-compatible changes and minor tweaks to conform to internal API shifts. - */ - public static readonly FALLBACK_VERSION = new com.keyman.utils.Version("10.0"); - public static readonly CURRENT_VERSION = new com.keyman.utils.Version("14.0"); - - /** - * The stub information to be passed into keyman.addKeyboards() in order to run the test. - */ - keyboard: KeyboardStub; - - /** - * The master array of test sets, each of which specifies constraints a client must fulfill for - * the tests contained therein to be valid. - */ - inputTestSets: TestSet[]; - - /** - * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. - * @param fromJSON - */ - constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { - if(!fromJSON) { - this.keyboard = null; - this.inputTestSets = []; - return; - } else if(typeof(fromJSON) == 'string') { - fromJSON = JSON.parse(fromJSON) as KeyboardTest; - } else if(fromJSON instanceof KeyboardStub) { - this.keyboard = fromJSON; - this.inputTestSets = []; - return; - } - - if(!fromJSON.specVersion) { - fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; - } else { - // Is serialized to a String when saved. - fromJSON.specVersion = new com.keyman.utils.Version(fromJSON.specVersion as unknown as string); - } - - this.keyboard = new KeyboardStub(fromJSON.keyboard); + /** + * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. + * @param fromJSON + */ + constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { + if(!fromJSON) { + this.keyboard = null; this.inputTestSets = []; - this.specVersion = fromJSON.specVersion; + return; + } else if(typeof(fromJSON) == 'string') { + fromJSON = JSON.parse(fromJSON) as KeyboardTest; + } else if(fromJSON instanceof KeyboardStub) { + this.keyboard = fromJSON; + this.inputTestSets = []; + return; + } - if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { - // Top-level test spec: EventSpecTestSet, based entirely on browser events. - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); - } - } else { - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); + if(!fromJSON.specVersion) { + fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; + } else { + // Is serialized to a String when saved. + fromJSON.specVersion = new utils.Version(fromJSON.specVersion as unknown as string); + } + + this.keyboard = new KeyboardStub(fromJSON.keyboard); + this.inputTestSets = []; + this.specVersion = fromJSON.specVersion; + + if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { + // Top-level test spec: EventSpecTestSet, based entirely on browser events. + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); + } + } else { + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); + } + } + } + + addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { + if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { + throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); + } + + for(var i=0; i < this.inputTestSets.length; i++) { + if(this.inputTestSets[i].constraint.equals(constraint)) { + this.inputTestSets[i].addTest(seq); + return; + } + } + + var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); + this.inputTestSets.push(newSet); + newSet.addTest(seq); + } + + test(proctor: Proctor) { + var setHasRun = false; + var failures: TestFailure[] = []; + + proctor.beforeAll(); + + // The original test spec requires a browser environment and thus requires its own `.run` implementation. + if(!(proctor.compatibleWithSuite(this))) { + throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); + } + + // Otherwise, the test spec instances will know how to run in any currently-supported environment. + for(var i = 0; i < this.inputTestSets.length; i++) { + var testSet = this.inputTestSets[i]; + + if(proctor.matchesTestSet(testSet)) { + var testFailures = testSet.test(proctor); + if(testFailures) { + failures = failures.concat(testFailures); } + setHasRun = true; } } - addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { - if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { - throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); - } - - for(var i=0; i < this.inputTestSets.length; i++) { - if(this.inputTestSets[i].constraint.equals(constraint)) { - this.inputTestSets[i].addTest(seq); - return; - } - } - - var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); - this.inputTestSets.push(newSet); - newSet.addTest(seq); + if(!setHasRun) { + // The sets CAN be empty, allowing silent failure if/when we actually want that. + console.warn("No test sets for this keyboard were applicable for this device!"); } - test(proctor: Proctor) { - var setHasRun = false; - var failures: TestFailure[] = []; - - proctor.beforeAll(); - - // The original test spec requires a browser environment and thus requires its own `.run` implementation. - if(!(proctor.compatibleWithSuite(this))) { - throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); - } - - // Otherwise, the test spec instances will know how to run in any currently-supported environment. - for(var i = 0; i < this.inputTestSets.length; i++) { - var testSet = this.inputTestSets[i]; - - if(proctor.matchesTestSet(testSet)) { - var testFailures = testSet.test(proctor); - if(testFailures) { - failures = failures.concat(testFailures); - } - setHasRun = true; - } - } - - if(!setHasRun) { - // The sets CAN be empty, allowing silent failure if/when we actually want that. - console.warn("No test sets for this keyboard were applicable for this device!"); - } - - // Allow the method's caller to trigger a 'fail'. - if(failures.length > 0) { - return failures; - } else { - return null; - } + // Allow the method's caller to trigger a 'fail'. + if(failures.length > 0) { + return failures; + } else { + return null; } + } - isEmpty() { - return this.inputTestSets.length == 0; - } + isEmpty() { + return this.inputTestSets.length == 0; + } - toPrettyJSON() { - return JSON.stringify(this, null, ' '); - } + toPrettyJSON() { + return JSON.stringify(this, null, ' '); + } - get isLegacy(): boolean { - return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); - } + get isLegacy(): boolean { + return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); } } \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 0ae4b5adcc..2031e450e4 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -1,94 +1,105 @@ +import Proctor, { AssertCallback } from "./proctor.js"; +import { + KeyboardTest, + TestSet, + TestSequence, + RecordedKeystrokeSequence, + RecordedPhysicalKeystroke, + RecordedSyntheticKeystroke +} from "./index.js"; -namespace KMWRecorder { - export class NodeProctor extends Proctor { - private keyboard: com.keyman.keyboards.Keyboard; - public __debug = false; +import Keyboard from "keyboard-processor/build/modules/keyboards/keyboard.js"; +import type KeyEvent from "keyboard-processor/build/modules/text/keyEvent.js"; +import KeyboardProcessor from "keyboard-processor/build/modules/text/keyboardProcessor.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; - constructor(keyboard: com.keyman.keyboards.Keyboard, device: com.keyman.utils.DeviceSpec, assert: AssertCallback) { - super(device, assert); +import DeviceSpec from "utils/build/modules/deviceSpec.js"; - this.keyboard = keyboard; - } +export default class NodeProctor extends Proctor { + private keyboard: Keyboard; + public __debug = false; - beforeAll() { - // - } - - before() { - // - } - - compatibleWithSuite(testSuite: KeyboardTest): boolean { - // Original-version tests did not supply core-compatible KeyEvent data. - return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION); - } - - get debugMode(): boolean { - return this.__debug; - } - - set debugMode(value: boolean) { - this.__debug = value; - } - - matchesTestSet(testSet: TestSet) { - // KeyboardProcessor is abstract enough to run tests aimed at any platform. - return true; - } - - simulateSequence(sequence: TestSequence, target?: com.keyman.text.OutputTarget): string { - // Start with an empty OutputTarget and a fresh KeyboardProcessor. - if(!target) { - target = new com.keyman.text.Mock(); - } - - // Establish a fresh processor, setting its keyboard appropriately for the test. - let processor = new com.keyman.text.KeyboardProcessor(this.device); - processor.activeKeyboard = this.keyboard; - - if(sequence instanceof RecordedKeystrokeSequence) { - for(let keystroke of sequence.inputs) { - let keyEvent: com.keyman.text.KeyEvent; - if(keystroke instanceof RecordedPhysicalKeystroke) { - // Use the keystroke's stored data to reconstruct the KeyEvent. - keyEvent = { - Lcode: keystroke.keyCode, - Lmodifiers: keystroke.modifiers, - LmodifierChange: keystroke.modifierChanged, - vkCode: keystroke.vkCode, - Lstates: keystroke.states, - kName: '', - device: this.device, - isSynthetic: false, - LisVirtualKey: this.keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards. - } - } else if(keystroke instanceof RecordedSyntheticKeystroke) { - let key = this.keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName); - keyEvent = key.constructKeyEvent(processor, this.device); - } - - // Fill in the final details of the KeyEvent... - keyEvent.device = this.device; - - // And now, execute the keystroke! - // We don't care too much about particularities of per-keystroke behavior yet. - // ... we _could_ if we wanted to, though. The framework is mostly in place; - // it's a matter of actually adding the feature. - let ruleBehavior = processor.processKeystroke(keyEvent, target); - - if(this.debugMode) { - console.log(JSON.stringify(target, null, ' ')); - console.log(JSON.stringify(ruleBehavior, null, ' ')); - } - } - } else { - throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present."); - } - return target.getText(); - } + constructor(keyboard: Keyboard, device: DeviceSpec, assert: AssertCallback) { + super(device, assert); + this.keyboard = keyboard; } -} -// Export the namespace itself, giving access to all contained classes. -module.exports = KMWRecorder; \ No newline at end of file + beforeAll() { + // + } + + before() { + // + } + + compatibleWithSuite(testSuite: KeyboardTest): boolean { + // Original-version tests did not supply core-compatible KeyEvent data. + return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION); + } + + get debugMode(): boolean { + return this.__debug; + } + + set debugMode(value: boolean) { + this.__debug = value; + } + + matchesTestSet(testSet: TestSet) { + // KeyboardProcessor is abstract enough to run tests aimed at any platform. + return true; + } + + simulateSequence(sequence: TestSequence, target?: OutputTarget): string { + // Start with an empty OutputTarget and a fresh KeyboardProcessor. + if(!target) { + target = new Mock(); + } + + // Establish a fresh processor, setting its keyboard appropriately for the test. + let processor = new KeyboardProcessor(this.device); + processor.activeKeyboard = this.keyboard; + + if(sequence instanceof RecordedKeystrokeSequence) { + for(let keystroke of sequence.inputs) { + let keyEvent: KeyEvent; + if(keystroke instanceof RecordedPhysicalKeystroke) { + // Use the keystroke's stored data to reconstruct the KeyEvent. + keyEvent = { + Lcode: keystroke.keyCode, + Lmodifiers: keystroke.modifiers, + LmodifierChange: keystroke.modifierChanged, + vkCode: keystroke.vkCode, + Lstates: keystroke.states, + kName: '', + device: this.device, + isSynthetic: false, + LisVirtualKey: this.keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards. + } + } else if(keystroke instanceof RecordedSyntheticKeystroke) { + let key = this.keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName); + keyEvent = key.constructKeyEvent(processor, this.device); + } + + // Fill in the final details of the KeyEvent... + keyEvent.device = this.device; + + // And now, execute the keystroke! + // We don't care too much about particularities of per-keystroke behavior yet. + // ... we _could_ if we wanted to, though. The framework is mostly in place; + // it's a matter of actually adding the feature. + let ruleBehavior = processor.processKeystroke(keyEvent, target); + + if(this.debugMode) { + console.log(JSON.stringify(target, null, ' ')); + console.log(JSON.stringify(ruleBehavior, null, ' ')); + } + } + } else { + throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present."); + } + return target.getText(); + } +} \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.tsconfig.json b/common/web/recorder/src/nodeProctor.tsconfig.json deleted file mode 100644 index 5b80993dc6..0000000000 --- a/common/web/recorder/src/nodeProctor.tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "../../../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "module": "none", - "outDir": "../build/nodeProctor/", - "outFile": "../build/nodeProctor/index.js", - "inlineSources": true, - "inlineSourceMap": true, - "target": "es5", - "types": ["node"], - "lib": ["es6"] - }, - - "files": [ - "index.ts", - "proctor.ts", - "nodeProctor.ts" - ], - - "references": [ - { "path": "../../keyman-version" }, - { "path": "../../utils" }, - { "path": "../../keyboard-processor/src" }, - { "path": "../../lm-message-types" } - ] -} diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index 38ffd9997a..bedc461237 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,50 +1,53 @@ -namespace KMWRecorder { - export type AssertCallback = (s1: any, s2: any, msg?: string) => void; +import { type DeviceSpec } from "utils/build/modules/index.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; + +import type { KeyboardTest, TestSet, TestSequence } from "./index.js"; + +export type AssertCallback = (s1: any, s2: any, msg?: string) => void; + +/** + * Facilitates running Recorder-generated tests on various platforms. + * + * Note that DOM-aware KeymanWeb will implement a Browser-based version, while + * keyboard-processor and input-processor will use a Node-based version instead. + */ +export default abstract class Proctor { + device: DeviceSpec; + + _assert: AssertCallback; + + constructor(device: DeviceSpec, assert: AssertCallback) { + this.device = device; + + this._assert = assert; + } + + assertEquals(s1: unknown, s2: unknown, msg?: string) { + if(this._assert) { + this._assert(s1, s2, msg); + } + } + + // Performs global test prep. + abstract beforeAll(); + + // Performs per-test setup + abstract before(); /** - * Facilitates running Recorder-generated tests on various platforms. - * - * Note that DOM-aware KeymanWeb will implement a Browser-based version, while - * keyboard-processor and input-processor will use a Node-based version instead. + * Allows the proctor to indicate if is capable of executing a suite of tests or not. + * @param testSuite */ - export abstract class Proctor { - device: com.keyman.utils.DeviceSpec; + abstract compatibleWithSuite(testSuite: KeyboardTest): boolean; - _assert: AssertCallback; + /** + * Indicates whether or not this Proctor is capable of running the specified set of tests. + */ + abstract matchesTestSet(testSet: TestSet); - constructor(device: com.keyman.utils.DeviceSpec, assert: AssertCallback) { - this.device = device; - - this._assert = assert; - } - - assertEquals(s1: unknown, s2: unknown, msg?: string) { - if(this._assert) { - this._assert(s1, s2, msg); - } - } - - // Performs global test prep. - abstract beforeAll(); - - // Performs per-test setup - abstract before(); - - /** - * Allows the proctor to indicate if is capable of executing a suite of tests or not. - * @param testSuite - */ - abstract compatibleWithSuite(testSuite: KeyboardTest): boolean; - - /** - * Indicates whether or not this Proctor is capable of running the specified set of tests. - */ - abstract matchesTestSet(testSet: TestSet); - - /** - * Simulates the specified test sequence for use in testing. - * @param sequence The recorded sequence, generally provided by a test set. - */ - abstract simulateSequence(sequence: TestSequence, target?: com.keyman.text.OutputTarget); - } + /** + * Simulates the specified test sequence for use in testing. + * @param sequence The recorded sequence, generally provided by a test set. + */ + abstract simulateSequence(sequence: TestSequence, target?: OutputTarget); } \ No newline at end of file diff --git a/common/web/recorder/src/tsconfig.json b/common/web/recorder/src/tsconfig.json deleted file mode 100644 index 00cb00a482..0000000000 --- a/common/web/recorder/src/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "../../../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "module": "none", - "outDir": "../build/", - "inlineSources": true, - "inlineSourceMap": true, - "target": "es5", - "types": ["node"], - "lib": ["es6"], - "outFile": "../build/index.js" - }, - - "files": [ - "index.ts", - "proctor.ts" - ], - - "references": [ - { "path": "../../keyman-version" }, - { "path": "../../utils" }, - { "path": "../../keyboard-processor/src" }, - { "path": "../../lm-message-types" } - ] -} diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json new file mode 100644 index 0000000000..c490c2280a --- /dev/null +++ b/common/web/recorder/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig-base.json", + + "compilerOptions": { + "allowJs": true, + "module": "es6", + "declaration": true, + "inlineSources": true, + "inlineSourceMap": true, + "target": "es5", + "types": ["node"], + "lib": ["es6"], + "baseUrl": "./", + "outDir": "build/modules/", + "tsBuildInfoFile": "build/modules/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + + "include": [ + "src/**/*.ts" + ], + + "references": [ + { "path": "../keyman-version" }, + { "path": "../utils/" }, + { "path": "../keyboard-processor/" }, + { "path": "../lm-message-types" } + ], + + "paths": { + "keyboard-processor": ["../keyboard-processor/build" ], + "keyman-version": ["../keyman-version/build" ], + "utils": ["../utils/build" ] + } +}