mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-11 11:25:34 +00:00
Merge pull request #9719 from keymanapp/fix/web/integration-test-fixes
fix(web): gesture-engine integration - automated test patchup 🐵
This commit is contained in:
commit
3ef85f34e7
19 changed files with 259 additions and 112 deletions
|
|
@ -108,7 +108,8 @@ export class GestureSequence<Type> extends EventEmitter<EventMap<Type>> {
|
|||
this.selector = selector;
|
||||
this.selector.on('rejectionwithaction', this.modelResetHandler);
|
||||
this.once('complete', () => {
|
||||
this.selector.off('rejectionwithaction', this.modelResetHandler)
|
||||
this.selector.off('rejectionwithaction', this.modelResetHandler);
|
||||
this.selector.dropSourcesWithIds(this.allSourceIds);
|
||||
|
||||
// Dropping the reference here gives us two benefits:
|
||||
// 1. Allows garbage collection to do its thing; this might be the last reference left to the selector instance.
|
||||
|
|
@ -130,7 +131,9 @@ export class GestureSequence<Type> extends EventEmitter<EventMap<Type>> {
|
|||
}
|
||||
|
||||
public get allSourceIds(): string[] {
|
||||
return this.stageReports[this.stageReports.length - 1]?.allSourceIds;
|
||||
// Note: there is a brief window of time - between construction & the deferred first
|
||||
// 'stage' event - during which this array may be of length 0.
|
||||
return this.stageReports[this.stageReports.length - 1]?.allSourceIds ?? [];
|
||||
}
|
||||
|
||||
private get baseGestureSetId(): string {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ManagedPromise } from "@keymanapp/web-utils";
|
|||
import { GestureSource, GestureSourceSubview } from "../../gestureSource.js";
|
||||
import { GestureMatcher, MatchResult, PredecessorMatch } from "./gestureMatcher.js";
|
||||
import { GestureModel } from "../specs/gestureModel.js";
|
||||
import { GestureSequence } from "./index.js";
|
||||
|
||||
interface GestureSourceTracker<Type> {
|
||||
/**
|
||||
|
|
@ -149,6 +150,14 @@ export class MatcherSelector<Type> extends EventEmitter<EventMap<Type>> {
|
|||
? [source instanceof GestureSourceSubview ? source.baseSource : source]
|
||||
: (source.sources as GestureSourceSubview<Type>[]).map((source) => source.baseSource);
|
||||
|
||||
if(sourceNotYetStaged) {
|
||||
// Cancellation before a first stage is possible; in this case, there's no sequence
|
||||
// to trigger cleanup. We can do that here.
|
||||
source.path.on('invalidated', () => {
|
||||
this.dropSourcesWithIds([source.identifier]);
|
||||
})
|
||||
}
|
||||
|
||||
const matchPromise = new ManagedPromise<MatcherSelection<Type>>();
|
||||
|
||||
/*
|
||||
|
|
@ -221,7 +230,10 @@ export class MatcherSelector<Type> extends EventEmitter<EventMap<Type>> {
|
|||
matcher: null,
|
||||
result: {
|
||||
matched: false,
|
||||
action: null
|
||||
action: {
|
||||
type: 'complete',
|
||||
item: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -280,6 +292,27 @@ export class MatcherSelector<Type> extends EventEmitter<EventMap<Type>> {
|
|||
this._sourceSelector.forEach((entry) => resetHooks(entry.source));
|
||||
}
|
||||
|
||||
public dropSourcesWithIds(idsToClean: string[]) {
|
||||
for(const id of idsToClean) {
|
||||
const index = this._sourceSelector.findIndex((entry) => entry.source.identifier);
|
||||
if(index > -1) {
|
||||
// Ensure that any pending MatcherSelector and/or GestureSequence promises dependent
|
||||
// on the source fully resolve (with cancellation).
|
||||
const droppedSelector = this._sourceSelector.splice(index, 1)[0];
|
||||
droppedSelector.matchPromise.resolve({
|
||||
matcher: null,
|
||||
result: {
|
||||
matched: false,
|
||||
action: {
|
||||
type: 'none',
|
||||
item: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private matchersForSource(source: GestureSource<Type>) {
|
||||
return this.potentialMatchers.filter((matcher) => {
|
||||
return !!matcher.sources.find((src) => src.identifier == source.identifier)
|
||||
|
|
|
|||
|
|
@ -306,9 +306,14 @@ describe("TouchpointCoordinator", () => {
|
|||
});
|
||||
|
||||
const runnerPromise = fakeClock.runToLastAsync();
|
||||
const sequence = await sequencePromise;
|
||||
const completeStub = sinon.fake();
|
||||
sequence.on('complete', completeStub); // was not called! Confirms a suspicion.
|
||||
|
||||
await Promise.all([runnerPromise, completionPromise1, completionPromise2]);
|
||||
|
||||
assert.isTrue(completeStub.calledOnce);
|
||||
|
||||
// Verify that all sources and sequences are cleared.
|
||||
assert.sameOrderedMembers(touchpointCoordinator.activeSources, []);
|
||||
assert.sameOrderedMembers(touchpointCoordinator.activeGestures, []);
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ describe('Engine - Basic Simulation', function() {
|
|||
if(!proctor.compatibleWithSuite(testSuite)) {
|
||||
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
|
||||
} else {
|
||||
it(set.toTestName(), function() {
|
||||
it(set.toTestName(), async function() {
|
||||
// Refresh the proctor instance at runtime.
|
||||
let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
|
||||
set.test(proctor);
|
||||
await set.test(proctor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ describe('Engine - Chirality', function() {
|
|||
if(!proctor.compatibleWithSuite(testSuite)) {
|
||||
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
|
||||
} else if(set.constraint.target == 'hardware') {
|
||||
it(set.toTestName(), function() {
|
||||
it(set.toTestName(), async function() {
|
||||
// Refresh the proctor instance at runtime.
|
||||
let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
|
||||
set.test(proctor);
|
||||
await set.test(proctor);
|
||||
});
|
||||
} else {
|
||||
it.skip(set.toTestName() + " - modifier state simulation for OSK not yet supported in headless KeyboardProcessor");
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ describe('Engine - Deadkeys', function() {
|
|||
if(!proctor.compatibleWithSuite(testSuite)) {
|
||||
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
|
||||
} else {
|
||||
it(set.toTestName(), function() {
|
||||
it(set.toTestName(), async function() {
|
||||
// Refresh the proctor instance at runtime.
|
||||
let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
|
||||
set.test(proctor);
|
||||
await set.test(proctor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export abstract class TestSequence<KeyRecord extends RecordedKeystroke | InputEv
|
|||
|
||||
abstract hasOSKInteraction(): boolean;
|
||||
|
||||
test(proctor: Proctor, target?: OutputTarget): {success: boolean, result: string} {
|
||||
async test(proctor: Proctor, target?: OutputTarget): Promise<{success: boolean, result: string}> {
|
||||
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
|
||||
if(!target) {
|
||||
target = new Mock();
|
||||
|
|
@ -220,7 +220,7 @@ export abstract class TestSequence<KeyRecord extends RecordedKeystroke | InputEv
|
|||
|
||||
proctor.before();
|
||||
|
||||
let result = proctor.simulateSequence(this, target);
|
||||
let result = await proctor.simulateSequence(this, target);
|
||||
proctor.assertEquals(result, this.output, this.msg);
|
||||
|
||||
return {success: (result == this.output), result: result};
|
||||
|
|
@ -527,7 +527,7 @@ export interface TestSet<Sequence extends TestSequence<any>> {
|
|||
|
||||
addTest(seq: Sequence): void;
|
||||
isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean): boolean;
|
||||
test(proctor: Proctor): TestFailure[];
|
||||
test(proctor: Proctor): Promise<TestFailure[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -563,13 +563,13 @@ export class EventSpecTestSet implements TestSet<InputEventSpecSequence> {
|
|||
}
|
||||
|
||||
// Validity should be checked before calling this method.
|
||||
test(proctor: Proctor): TestFailure[] {
|
||||
async test(proctor: Proctor): Promise<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);
|
||||
var simResult = await testSet[i].test(proctor);
|
||||
if(!simResult.success) {
|
||||
// Failed test!
|
||||
failures.push(new TestFailure(this.constraint, testSeq, simResult.result));
|
||||
|
|
@ -613,13 +613,13 @@ export class RecordedSequenceTestSet implements TestSet<RecordedKeystrokeSequenc
|
|||
}
|
||||
|
||||
// Validity should be checked before calling this method.
|
||||
test(proctor: Proctor): TestFailure[] {
|
||||
async test(proctor: Proctor): Promise<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);
|
||||
var simResult = await testSet[i].test(proctor);
|
||||
if(!simResult.success) {
|
||||
// Failed test!
|
||||
failures.push(new TestFailure(this.constraint, testSeq, simResult.result));
|
||||
|
|
@ -729,11 +729,11 @@ export class KeyboardTest {
|
|||
newSet.addTest(seq);
|
||||
}
|
||||
|
||||
test(proctor: Proctor) {
|
||||
async test(proctor: Proctor) {
|
||||
var setHasRun = false;
|
||||
var failures: TestFailure[] = [];
|
||||
|
||||
proctor.beforeAll();
|
||||
await proctor.beforeAll();
|
||||
|
||||
// The original test spec requires a browser environment and thus requires its own `.run` implementation.
|
||||
if(!(proctor.compatibleWithSuite(this))) {
|
||||
|
|
@ -745,7 +745,7 @@ export class KeyboardTest {
|
|||
var testSet = this.inputTestSets[i];
|
||||
|
||||
if(proctor.matchesTestSet(testSet)) {
|
||||
var testFailures = testSet.test(proctor);
|
||||
var testFailures = await testSet.test(proctor);
|
||||
if(testFailures) {
|
||||
failures = failures.concat(testFailures);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default class NodeProctor extends Proctor {
|
|||
this.keyboardWithHarness = kbdHarness;
|
||||
}
|
||||
|
||||
beforeAll() {
|
||||
async beforeAll() {
|
||||
//
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export default class NodeProctor extends Proctor {
|
|||
return true;
|
||||
}
|
||||
|
||||
simulateSequence(sequence: TestSequence<any>, target?: OutputTarget): string {
|
||||
async simulateSequence(sequence: TestSequence<any>, target?: OutputTarget): Promise<string> {
|
||||
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
|
||||
if(!target) {
|
||||
target = new Mock();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default abstract class Proctor {
|
|||
}
|
||||
|
||||
// Performs global test prep.
|
||||
abstract beforeAll();
|
||||
abstract beforeAll(): Promise<void>;
|
||||
|
||||
// Performs per-test setup
|
||||
abstract before();
|
||||
|
|
@ -49,5 +49,5 @@ export default abstract class Proctor {
|
|||
* Simulates the specified test sequence for use in testing.
|
||||
* @param sequence The recorded sequence, generally provided by a test set.
|
||||
*/
|
||||
abstract simulateSequence(sequence: TestSequence<any>, target?: OutputTarget);
|
||||
abstract simulateSequence(sequence: TestSequence<any>, target?: OutputTarget): Promise<string>;
|
||||
}
|
||||
|
|
@ -184,7 +184,17 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
|
||||
public deactivateCurrentTarget() {
|
||||
const priorTarget = this.activeTarget || this.lastActiveTarget;
|
||||
if(priorTarget) {
|
||||
|
||||
/* During integrated tests, it was possible in the past for a `beforeAll`
|
||||
* -initialized KMW to reach this state between tests. The target fixture
|
||||
* got cleared, but the `mostRecentTarget` / `lastActiveTarget` was not
|
||||
* - just the `currentTarget` / `activeTarget`. See #9718.
|
||||
*
|
||||
* Newly-added code in `forgetActiveTarget` seeks to prevent this scenario,
|
||||
* but as there's no consistent repro to prove it sufficient, an appropriate
|
||||
* guard-condition has been added here too.
|
||||
*/
|
||||
if(priorTarget && this.page.isAttached(priorTarget.getElement())) {
|
||||
this._BlurKeyboardSettings(priorTarget.getElement());
|
||||
}
|
||||
|
||||
|
|
@ -198,12 +208,19 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
this.focusAssistant.maintainingFocus = false;
|
||||
this.focusAssistant.restoringFocus = false;
|
||||
|
||||
const priorTarget = this.activeTarget || this.lastActiveTarget;
|
||||
const priorTarget = this.activeTarget || this.mostRecentTarget;
|
||||
if(priorTarget) {
|
||||
this._BlurKeyboardSettings(priorTarget.getElement());
|
||||
}
|
||||
|
||||
// Will ensure that the element is no longer active. Does not erase
|
||||
// it from being the `lastActiveTarget`, though.
|
||||
this.setActiveTarget(null, true);
|
||||
|
||||
// So we erase it here.
|
||||
if(priorTarget == this.lastActiveTarget) {
|
||||
this.mostRecentTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
public setActiveTarget(target: OutputTarget<any>, sendEvents: boolean) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DeviceSpec } from '@keymanapp/web-utils';
|
||||
import { DeviceSpec, ManagedPromise } from '@keymanapp/web-utils';
|
||||
import { type InternalKeyboardFont as KeyboardFont } from '@keymanapp/keyboard-processor';
|
||||
|
||||
type FontFamilyStyleMap = {[family: string]: HTMLStyleElement};
|
||||
|
|
@ -28,6 +28,27 @@ export class StylesheetManager {
|
|||
this.linkNode.appendChild(sheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a `Promise` that resolves when all currently-linked stylesheets have loaded.
|
||||
* Any change to the set of linked sheets after the initial call will be ignored.
|
||||
*/
|
||||
async allLoadedPromise() {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for(const sheetElem of this.linkedSheets) {
|
||||
// Based on https://stackoverflow.com/a/21147238
|
||||
if(sheetElem.sheet?.cssRules) {
|
||||
promises.push(Promise.resolve());
|
||||
} else {
|
||||
const promise = new ManagedPromise<void>();
|
||||
sheetElem.addEventListener('load', () => promise.resolve());
|
||||
promises.push(promise.corePromise);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a stylesheet with a font-face CSS descriptor for the embedded font appropriate
|
||||
* for the browser being used
|
||||
|
|
@ -212,6 +233,8 @@ export class StylesheetManager {
|
|||
sheet.parentNode.removeChild(sheet);
|
||||
}
|
||||
}
|
||||
|
||||
this.linkedSheets.splice(0, this.linkedSheets.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,28 +104,17 @@ export default class OSKLayerGroup {
|
|||
private nearestKey(coord: Omit<InputSample<KeyElement>, 'item'>, layer: OSKLayer): KeyElement {
|
||||
const baseRect = this.element.getBoundingClientRect();
|
||||
|
||||
/**
|
||||
* Transforms the client rect of child elements to use a coordinate system where the top-left
|
||||
* of the layer group's bounding rectangle serves as the origin - the same coordinate
|
||||
* system output by the gesture engine.
|
||||
* @param childRect
|
||||
* @returns
|
||||
*/
|
||||
const translation = (childRect: DOMRect) => {
|
||||
return new DOMRect(childRect.x - baseRect.x, childRect.y - baseRect.y, childRect.width, childRect.height);
|
||||
}
|
||||
|
||||
let row: OSKRow = null;
|
||||
let bestMatchDistance = Number.MAX_VALUE;
|
||||
|
||||
// Find the row that the touch-coordinate lies within.
|
||||
for(const r of layer.rows) {
|
||||
const rowRect = translation(r.element.getBoundingClientRect());
|
||||
if(rowRect.top <= coord.targetY && coord.targetY < rowRect.bottom) {
|
||||
const rowRect = r.element.getBoundingClientRect();
|
||||
if(rowRect.top <= coord.clientY && coord.clientY < rowRect.bottom) {
|
||||
row = r;
|
||||
break;
|
||||
} else {
|
||||
const distance = rowRect.top > coord.targetY ? rowRect.top - coord.targetY : coord.targetY - rowRect.bottom;
|
||||
const distance = rowRect.top > coord.clientY ? rowRect.top - coord.clientY : coord.clientY - rowRect.bottom;
|
||||
|
||||
if(distance < bestMatchDistance) {
|
||||
bestMatchDistance = distance;
|
||||
|
|
@ -144,12 +133,12 @@ export default class OSKLayerGroup {
|
|||
let dxMax = 24;
|
||||
let dxMin = 100000;
|
||||
|
||||
const x = coord.targetX;
|
||||
const x = coord.clientX;
|
||||
|
||||
for (let k = 0; k < row.keys.length; k++) {
|
||||
// Second-biggest, though documentation suggests this is probably right.
|
||||
const keySquare = row.keys[k].square as HTMLElement; // gets the .kmw-key-square containing a key
|
||||
const squareRect = translation(keySquare.getBoundingClientRect());
|
||||
const squareRect = keySquare.getBoundingClientRect();
|
||||
|
||||
// Find the actual key element.
|
||||
let childNode = keySquare.firstChild ? keySquare.firstChild as HTMLElement : keySquare;
|
||||
|
|
@ -179,7 +168,7 @@ export default class OSKLayerGroup {
|
|||
|
||||
if (dxMin < 100000) {
|
||||
const t = <HTMLElement>row.keys[closestKeyIndex].square;
|
||||
const squareRect = translation(t.getBoundingClientRect());
|
||||
const squareRect = t.getBoundingClientRect();
|
||||
|
||||
const x1 = squareRect.left;
|
||||
const x2 = squareRect.right;
|
||||
|
|
|
|||
|
|
@ -698,14 +698,17 @@ export default abstract class OSKView extends EventEmitter<EventMap> implements
|
|||
// Instantly resets the OSK container, erasing / delinking the previously-loaded keyboard.
|
||||
this._Box.innerHTML = '';
|
||||
|
||||
// Since we cleared all inner HTML, that means we cleared the stylesheets, too.
|
||||
this.uiStyleSheetManager.unlinkAll();
|
||||
this.kbdStyleSheetManager.unlinkAll();
|
||||
|
||||
// Install the default OSK stylesheets - but don't have it managed by the keyboard-specific stylesheet manager.
|
||||
// We wish to maintain kmwosk.css whenever keyboard-specific styles are reset/removed.
|
||||
// Temp-hack: embedded products prefer their stylesheet, etc linkages without the /osk path component.
|
||||
let subpath = 'osk/';
|
||||
if(this.config.isEmbedded) {
|
||||
subpath = '';
|
||||
}
|
||||
|
||||
// Install the default OSK stylesheet - but don't have it managed by the keyboard-specific stylesheet manager.
|
||||
// We wish to maintain kmwosk.css whenever keyboard-specific styles are reset/removed.
|
||||
for(let sheetFile of OSKView.STYLESHEET_FILES) {
|
||||
const sheetHref = `${this.config.pathConfig.resources}/${subpath}${sheetFile}`;
|
||||
this.uiStyleSheetManager.linkExternalSheet(sheetHref);
|
||||
|
|
|
|||
|
|
@ -178,31 +178,31 @@ describe('Engine - Browser Interactions', function() {
|
|||
assert.equal(inputElem.value, "ຫ");
|
||||
});
|
||||
|
||||
it('Simple OSK click', function() {
|
||||
it('Simple OSK click', async 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);
|
||||
|
||||
let eventDriver = new KMWRecorder.BrowserDriver(inputElem);
|
||||
eventDriver.simulateEvent(lao_s_event);
|
||||
await eventDriver.simulateEvent(lao_s_event);
|
||||
|
||||
if(inputElem['base']) {
|
||||
inputElem = inputElem['base'];
|
||||
}
|
||||
assert.equal(inputElem.value, ";");
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
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', async function() {
|
||||
return await 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', async function() {
|
||||
return await runKeyboardTestFromJSON('/engine_tests/basic_lao_simulation.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -231,10 +231,10 @@ describe('Unmatched Final Groups', function() {
|
|||
fixture.cleanup();
|
||||
});
|
||||
|
||||
it('matches rule from early group AND performs default behavior', function() {
|
||||
it('matches rule from early group AND performs default behavior', async 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);
|
||||
return await runKeyboardTestFromJSON('/engine_tests/ghp_enter.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,23 +30,23 @@ describe('Engine - Chirality', function() {
|
|||
fixture.cleanup();
|
||||
});
|
||||
|
||||
it('Keyboard + OSK simulation', function() {
|
||||
it('Keyboard + OSK simulation', async 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',
|
||||
return await runKeyboardTestFromJSON('/engine_tests/chirality.json',
|
||||
{usingOSK: false},
|
||||
assert.equal,
|
||||
testconfig.timeouts.scriptLoad).then(() => {
|
||||
testconfig.timeouts.scriptLoad).then(async () => {
|
||||
/* 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);
|
||||
return await runKeyboardTestFromJSON('/engine_tests/chirality.json', {usingOSK: true}, assert.equal, testconfig.timeouts.scriptLoad);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ var assert = chai.assert;
|
|||
|
||||
import {
|
||||
loadKeyboardFromJSON,
|
||||
oskResourceLoadPromise,
|
||||
setupKMW,
|
||||
teardownKMW
|
||||
} from "../test_utils.js";
|
||||
|
|
@ -49,7 +50,7 @@ describe('Event Management', function() {
|
|||
assert.isNull(ele.onchange, '`onchange` handler was not called');
|
||||
});
|
||||
|
||||
it('OSK-based onChange event generation', function() {
|
||||
it('OSK-based onChange event generation', async function() {
|
||||
var simple_A = {"type":"osk","keyID":"default-K_A"};
|
||||
var event = new KMWRecorder.OSKInputEventSpec(simple_A);
|
||||
|
||||
|
|
@ -61,8 +62,13 @@ describe('Event Management', function() {
|
|||
|
||||
keyman.setActiveElement(ele);
|
||||
|
||||
// Browsers will only start loading OSK resources (the CSS) once both a keyboard and target
|
||||
// are set... and that's an async operation.
|
||||
await oskResourceLoadPromise();
|
||||
|
||||
// OSK CSS is needed for successful simulation for integration tests involving the gesture engine.
|
||||
let eventDriver = new KMWRecorder.BrowserDriver(ele);
|
||||
eventDriver.simulateEvent(event);
|
||||
await eventDriver.simulateEvent(event);
|
||||
|
||||
let focusEvent = new FocusEvent('blur', {relatedTarget: ele});
|
||||
ele.dispatchEvent(focusEvent);
|
||||
|
|
@ -94,7 +100,7 @@ describe('Event Management', function() {
|
|||
assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
|
||||
});
|
||||
|
||||
it('OSK-based onInput event generation', function() {
|
||||
it('OSK-based onInput event generation', async function() {
|
||||
var simple_A = {"type":"osk","keyID":"default-K_A"};
|
||||
var event = new KMWRecorder.OSKInputEventSpec(simple_A);
|
||||
|
||||
|
|
@ -109,9 +115,9 @@ describe('Event Management', function() {
|
|||
});
|
||||
|
||||
let eventDriver = new KMWRecorder.BrowserDriver(ele);
|
||||
eventDriver.simulateEvent(event);
|
||||
eventDriver.simulateEvent(event);
|
||||
eventDriver.simulateEvent(event);
|
||||
await eventDriver.simulateEvent(event);
|
||||
await eventDriver.simulateEvent(event);
|
||||
await eventDriver.simulateEvent(event);
|
||||
|
||||
assert.equal(counterObj.i, fin, "Event handler not called the expected number of times");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -201,31 +201,32 @@ export async function loadKeyboardFromJSON(jsonPath, timeout, params) {
|
|||
return loadKeyboardStub(stub, timeout, params);
|
||||
}
|
||||
|
||||
function runLoadedKeyboardTest(testDef, device, usingOSK, assertCallback) {
|
||||
async function runLoadedKeyboardTest(testDef, device, usingOSK, assertCallback) {
|
||||
var inputElem = document.getElementById('singleton');
|
||||
|
||||
let proctor = new KMWRecorder.BrowserProctor(inputElem, device, usingOSK, assertCallback);
|
||||
testDef.test(proctor);
|
||||
await testDef.test(proctor);
|
||||
}
|
||||
|
||||
export function runKeyboardTestFromJSON(jsonPath, params, assertCallback, timeout) {
|
||||
export async function runKeyboardTestFromJSON(jsonPath, params, assertCallback, timeout) {
|
||||
var testSpec = new KMWRecorder.KeyboardTest(fixture.load(jsonPath, true));
|
||||
let device = new Device();
|
||||
device.detect();
|
||||
|
||||
return loadKeyboardStub(testSpec.keyboard, timeout).then(() => {
|
||||
runLoadedKeyboardTest(testSpec, device.coreSpec, params.usingOSK, assertCallback);
|
||||
return runLoadedKeyboardTest(testSpec, device.coreSpec, params.usingOSK, assertCallback);
|
||||
}).finally(() => {
|
||||
keyman.removeKeyboards(testSpec.keyboard.id);
|
||||
});
|
||||
}
|
||||
|
||||
// function retrieveAndReset(Pelem) {
|
||||
// let val = Pelem.value;
|
||||
// Pelem.value = "";
|
||||
|
||||
// return val;
|
||||
// }
|
||||
export async function oskResourceLoadPromise() {
|
||||
// If the CSS isn't fully loaded, the element positions will not match their expected
|
||||
// locations in the keyboard layout and OSK keys won't be triggered properly by the
|
||||
// gesture engine.
|
||||
const styleManager = keyman.osk['uiStyleSheetManager']; // is private
|
||||
await styleManager.allLoadedPromise();
|
||||
}
|
||||
|
||||
// Useful for tests related to strings with supplementary pairs.
|
||||
export function toSupplementaryPairString(code) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,23 @@ import {
|
|||
OSKInputEventSpec,
|
||||
PhysicalInputEventSpec
|
||||
} from "@keymanapp/recorder-core";
|
||||
import { ManagedPromise, timedPromise } from "@keymanapp/web-utils";
|
||||
|
||||
import { type KeymanEngine } from 'keyman/app/browser';
|
||||
|
||||
declare var keyman: KeymanEngine;
|
||||
|
||||
function asTouchList(arr: any[]) {
|
||||
return {
|
||||
get length() {
|
||||
return arr.length;
|
||||
},
|
||||
|
||||
item(index: number) {
|
||||
return arr[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
export class BrowserDriver {
|
||||
static readonly physicalEventClass: string = "KeyboardEvent";
|
||||
static readonly physicalEventType: string = "keydown";
|
||||
|
|
@ -26,13 +38,13 @@ export class BrowserDriver {
|
|||
this.target = target;
|
||||
}
|
||||
|
||||
simulateEvent(eventSpec: InputEventSpec) {
|
||||
async simulateEvent(eventSpec: InputEventSpec) {
|
||||
switch(eventSpec.type) {
|
||||
case "key":
|
||||
this.simulateHardwareEvent(eventSpec as PhysicalInputEventSpec);
|
||||
break;
|
||||
case "osk":
|
||||
this.simulateOSKEvent(eventSpec as OSKInputEventSpec);
|
||||
await this.simulateOSKEvent(eventSpec as OSKInputEventSpec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,48 +61,96 @@ export class BrowserDriver {
|
|||
this.target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
simulateOSKEvent(eventSpec: OSKInputEventSpec) {
|
||||
let target = this.target;
|
||||
let oskKeyElement = document.getElementById(eventSpec.keyID);
|
||||
async simulateOSKEvent(eventSpec: OSKInputEventSpec) {
|
||||
const originalLayer = keyman.osk.vkbd.layerId;
|
||||
|
||||
if(!oskKeyElement) {
|
||||
console.error('Could not find OSK key "' + eventSpec.keyID + '"!');
|
||||
// The following lines will throw an appropriate-enough error.
|
||||
return;
|
||||
// Calculations go wrong if the key's layer is not visible.
|
||||
const keyID = eventSpec.keyID;
|
||||
const targetLayer = keyID.indexOf('-') == -1 ? keyID : keyID.substring(0, keyID.lastIndexOf('-'));
|
||||
|
||||
if(targetLayer != originalLayer) {
|
||||
keyman.osk.vkbd.layerGroup.layers[originalLayer].element.style.display = 'none';
|
||||
keyman.osk.vkbd.layerGroup.layers[targetLayer].element.style.display = 'block';
|
||||
keyman.osk.vkbd.layerId = targetLayer;
|
||||
|
||||
// Only the "current" layer of the OSK is laid out on refresh; a non-default
|
||||
// layer won't have proper layout before this!
|
||||
keyman.osk.vkbd.refreshLayout();
|
||||
}
|
||||
|
||||
// To be safe, we replicate the MouseEvent similarly to the keystroke event.
|
||||
var downEvent;
|
||||
var upEvent;
|
||||
if(keyman.config.hostDevice.touchable) {
|
||||
downEvent = new Event(BrowserDriver.oskDownTouchType);
|
||||
upEvent = new Event(BrowserDriver.oskUpTouchType);
|
||||
downEvent['touches'] = [{"target": oskKeyElement}];
|
||||
upEvent['touches'] = [{"target": oskKeyElement}];
|
||||
downEvent['changedTouches'] = [{"target": oskKeyElement}];
|
||||
upEvent['changedTouches'] = [{"target": oskKeyElement}];
|
||||
} else {
|
||||
downEvent = new Event(BrowserDriver.oskDownMouseType);
|
||||
upEvent = new Event(BrowserDriver.oskUpMouseType);
|
||||
downEvent['relatedTarget'] = target;
|
||||
upEvent['relatedTarget'] = target;
|
||||
// Mouse-click driven OSK use involves use of at least one mouse button.
|
||||
downEvent['button'] = upEvent['button'] = 0;
|
||||
downEvent['buttons'] = 1;
|
||||
upEvent['buttons'] = 0;
|
||||
}
|
||||
try {
|
||||
let target = this.target;
|
||||
let oskKeyElement = document.getElementById(eventSpec.keyID);
|
||||
const boundingBox = oskKeyElement.getBoundingClientRect();
|
||||
const center = {
|
||||
clientX: boundingBox.left + boundingBox.width/2,
|
||||
clientY: boundingBox.top + boundingBox.height/2
|
||||
}
|
||||
|
||||
oskKeyElement.dispatchEvent(downEvent);
|
||||
oskKeyElement.dispatchEvent(upEvent);
|
||||
if(!oskKeyElement) {
|
||||
console.error('Could not find OSK key "' + eventSpec.keyID + '"!');
|
||||
// The following lines will throw an appropriate-enough error.
|
||||
return;
|
||||
}
|
||||
|
||||
// To be safe, we replicate the MouseEvent similarly to the keystroke event.
|
||||
var downEvent;
|
||||
var upEvent;
|
||||
if(keyman.config.hostDevice.touchable) {
|
||||
downEvent = new Event(BrowserDriver.oskDownTouchType);
|
||||
upEvent = new Event(BrowserDriver.oskUpTouchType);
|
||||
downEvent['touches'] = asTouchList([{"target": oskKeyElement, ...center}]);
|
||||
upEvent['touches'] = asTouchList([{"target": oskKeyElement, ...center}]);
|
||||
downEvent['changedTouches'] = asTouchList([{"target": oskKeyElement, ...center}]);
|
||||
upEvent['changedTouches'] = asTouchList([{"target": oskKeyElement, ...center}]);
|
||||
} else {
|
||||
downEvent = new Event(BrowserDriver.oskDownMouseType);
|
||||
upEvent = new Event(BrowserDriver.oskUpMouseType);
|
||||
downEvent.clientX = center.clientX;
|
||||
downEvent.clientY = center.clientY;
|
||||
downEvent['relatedTarget'] = target;
|
||||
upEvent.clientX = center.clientX;
|
||||
upEvent.clientY = center.clientY;
|
||||
upEvent['relatedTarget'] = target;
|
||||
// Mouse-click driven OSK use involves use of at least one mouse button.
|
||||
downEvent['button'] = upEvent['button'] = 0;
|
||||
downEvent['buttons'] = 1;
|
||||
upEvent['buttons'] = 0;
|
||||
}
|
||||
|
||||
oskKeyElement.dispatchEvent(downEvent);
|
||||
oskKeyElement.dispatchEvent(upEvent);
|
||||
|
||||
// Note: our gesture engine's internal structure means that even simple keystrokes like this
|
||||
// involve async processing. We'll need to sync up.
|
||||
const defermentPromise = new ManagedPromise<void>();
|
||||
|
||||
// Easiest way to resolve? Just wait for the key event. Currently, our integration tests
|
||||
// at this level only use simple-taps, so there shouldn't be any cases that emit multiple
|
||||
// key events at this time.
|
||||
keyman.osk.on('keyevent', () => { defermentPromise.resolve() });
|
||||
|
||||
// Just in case something goes wrong and no event occurs, we apply a timeout to keep
|
||||
// the tests moving along.
|
||||
await Promise.race([defermentPromise.corePromise, timedPromise(50)]);
|
||||
} finally {
|
||||
// The alt-layer needs to be maintained until the key is generated.
|
||||
if(targetLayer != originalLayer) {
|
||||
keyman.osk.vkbd.layerGroup.layers[targetLayer].element.style.display = 'none';
|
||||
keyman.osk.vkbd.layerGroup.layers[originalLayer].element.style.display = 'block';
|
||||
keyman.osk.vkbd.layerId = originalLayer;
|
||||
keyman.osk.vkbd.refreshLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execution of a test sequence depends on the testing environment; integrated
|
||||
// testing requires browser-specific code.
|
||||
simulateSequence(sequence: InputEventSpecSequence): string {
|
||||
async simulateSequence(sequence: InputEventSpecSequence): Promise<string> {
|
||||
let ele = this.target;
|
||||
|
||||
for(var i=0; i < sequence.inputs.length; i++) {
|
||||
this.simulateEvent(sequence.inputs[i]);
|
||||
await this.simulateEvent(sequence.inputs[i]);
|
||||
}
|
||||
|
||||
if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
TestSet
|
||||
} from "@keymanapp/recorder-core";
|
||||
import { BrowserDriver } from "./browserDriver.js";
|
||||
import { type StylesheetManager } from "keyman/engine/dom-utils";
|
||||
|
||||
|
||||
type AssertCallback = (s1: any, s2: any, msg?: string) => void;
|
||||
|
|
@ -51,9 +52,15 @@ export class BrowserProctor extends Proctor {
|
|||
}
|
||||
|
||||
// Performs browser-specific global test prep.
|
||||
beforeAll() {
|
||||
async beforeAll() {
|
||||
let ele = this.target;
|
||||
(window['keyman'] as any).setActiveElement(ele['base'] ? ele['base'] : ele);
|
||||
keyman.setActiveElement(ele, true);
|
||||
|
||||
// If the CSS isn't fully loaded, the element positions will not match their expected
|
||||
// locations in the keyboard layout and OSK keys won't be triggered properly by the
|
||||
// gesture engine.
|
||||
const styleManager = keyman.osk['uiStyleSheetManager'] as StylesheetManager;
|
||||
await styleManager.allLoadedPromise();
|
||||
}
|
||||
|
||||
before() {
|
||||
|
|
@ -74,12 +81,12 @@ export class BrowserProctor extends Proctor {
|
|||
|
||||
// Execution of a test sequence depends on the testing environment; this handles
|
||||
// the browser-specific aspects.
|
||||
simulateSequence(sequence: TestSequence<any>, outputTarget?: OutputTarget): string {
|
||||
async simulateSequence(sequence: TestSequence<any>, outputTarget?: OutputTarget): Promise<string> {
|
||||
let driver = new BrowserDriver(this.target);
|
||||
|
||||
// For the version 10.0 spec
|
||||
if(sequence instanceof InputEventSpecSequence) {
|
||||
return driver.simulateSequence(sequence);
|
||||
return await driver.simulateSequence(sequence);
|
||||
|
||||
// For the version 14.0+ spec
|
||||
} else if(sequence instanceof RecordedKeystrokeSequence) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue