mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-08 00:27:42 +00:00
feat(web): further gesture-selection work - single source / first stage now vetted
This commit is contained in:
parent
d03cd7f155
commit
978965fd5e
7 changed files with 518 additions and 7 deletions
|
|
@ -82,8 +82,8 @@ export class GestureSource<HoveredItemType> {
|
|||
}
|
||||
|
||||
public update(sample: InputSample<HoveredItemType>) {
|
||||
this.path.extend(sample);
|
||||
this._baseItem ||= sample.item;
|
||||
this.path.extend(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,7 +111,7 @@ export class GestureSource<HoveredItemType> {
|
|||
* from the most recently-observed path coordinate.
|
||||
* @returns
|
||||
*/
|
||||
public constructSubview(startAtEnd: boolean, preserveBaseItem: boolean) {
|
||||
public constructSubview(startAtEnd: boolean, preserveBaseItem: boolean): GestureSourceSubview<HoveredItemType> {
|
||||
return new GestureSourceSubview(this, startAtEnd, preserveBaseItem);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { GestureMatcher } from './gestureMatcher.js';
|
||||
export { PathMatcher } from './pathMatcher.js';
|
||||
export { PathMatcher } from './pathMatcher.js';
|
||||
export { MatcherSelection, MatcherSelector } from './matcherSelector.js';
|
||||
|
|
@ -106,7 +106,9 @@ export class PathMatcher<Type> {
|
|||
return this.finalize(false, 'path');
|
||||
}
|
||||
|
||||
if(model.itemChangeAction && source.currentSample.item != source.baseItem) {
|
||||
// For certain unit-test setups, we may have a zero-length path when this is called during test init.
|
||||
// It's best to have that path-coord-length check in place, just in case.
|
||||
if(model.itemChangeAction && source.path.coords.length > 0 && source.currentSample.item != source.baseItem) {
|
||||
const result = model.itemChangeAction == 'resolve';
|
||||
|
||||
return this.finalize(result, 'item');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { assert } from 'chai'
|
||||
import { GestureSource, InputSample } from '@keymanapp/gesture-recognizer';
|
||||
|
||||
// Should probably be a bit more thorough, but it's a start.
|
||||
describe("GestureSource", function() {
|
||||
describe(".constructSubview", function() {
|
||||
it("properly propagates updates", () => {
|
||||
let source = new GestureSource<string>(0, true);
|
||||
let subview = source.constructSubview(true, true);
|
||||
|
||||
assert.equal(subview.path.coords.length, 0);
|
||||
|
||||
let sample: InputSample<string> = {
|
||||
targetX: 1,
|
||||
targetY: 2,
|
||||
item: 'hello',
|
||||
t: 101
|
||||
};
|
||||
|
||||
source.update(sample);
|
||||
|
||||
assert.equal(subview.path.coords.length, 1);
|
||||
assert.equal(subview.currentSample, sample);
|
||||
});
|
||||
|
||||
it("propagates path termination (complete)", () => {
|
||||
let source = new GestureSource<string>(0, true);
|
||||
let subview = source.constructSubview(true, true);
|
||||
let subview2 = source.constructSubview(true, true);
|
||||
|
||||
assert.equal(subview.path.coords.length, 0);
|
||||
|
||||
let sample: InputSample<string> = {
|
||||
targetX: 1,
|
||||
targetY: 2,
|
||||
item: 'hello',
|
||||
t: 101
|
||||
};
|
||||
|
||||
source.update(sample);
|
||||
subview.terminate(false);
|
||||
|
||||
assert.equal(subview.path.coords.length, 1);
|
||||
assert.equal(subview.currentSample, sample);
|
||||
assert.equal(subview.isPathComplete, true);
|
||||
assert.equal(subview.path.wasCancelled, false);
|
||||
assert.equal(subview2.isPathComplete, true);
|
||||
assert.equal(subview2.path.wasCancelled, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -45,7 +45,7 @@ export const LongpressModel: GestureModel = {
|
|||
|
||||
export const MultitapModel: GestureModel = {
|
||||
id: 'multitap',
|
||||
resolutionPriority: 1,
|
||||
resolutionPriority: 2,
|
||||
itemPriority: 1,
|
||||
contacts: [
|
||||
{
|
||||
|
|
@ -75,7 +75,7 @@ export const MultitapModel: GestureModel = {
|
|||
|
||||
export const SimpleTapModel: GestureModel = {
|
||||
id: 'simple-tap',
|
||||
resolutionPriority: 0,
|
||||
resolutionPriority: 1,
|
||||
itemPriority: 0,
|
||||
contacts: [
|
||||
{
|
||||
|
|
@ -91,7 +91,14 @@ export const SimpleTapModel: GestureModel = {
|
|||
],
|
||||
resolutionAction: {
|
||||
type: 'optional-chain',
|
||||
allowNext: 'multitap'
|
||||
allowNext: 'multitap',
|
||||
item: 'current'
|
||||
},
|
||||
rejectionActions: {
|
||||
item: {
|
||||
type: 'optional-chain',
|
||||
allowNext: 'simple-tap'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
import { assert } from 'chai'
|
||||
import sinon from 'sinon';
|
||||
|
||||
import * as PromiseStatusModule from 'promise-status-async';
|
||||
const PromiseStatuses = PromiseStatusModule.PromiseStatuses;
|
||||
import { assertingPromiseStatus as promiseStatus } from '../../../resources/assertingPromiseStatus.js';
|
||||
|
||||
import { simulateMultiSourceSelectorInput } from "../../../resources/simulateMultiSourceInput.js";
|
||||
|
||||
import { GestureSource, gestures } from '@keymanapp/gesture-recognizer';
|
||||
|
||||
import { TouchpathTurtle } from '#tools';
|
||||
|
||||
type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
|
||||
type MatcherSelector<Type> = gestures.matchers.MatcherSelector<Type>;
|
||||
type GestureModel<Type> = gestures.specs.GestureModel<Type>;
|
||||
|
||||
import {
|
||||
LongpressModel,
|
||||
MultitapModel,
|
||||
SimpleTapModel,
|
||||
SubkeySelectModel
|
||||
} from './isolatedGestureSpecs.js';
|
||||
|
||||
import {
|
||||
LongpressDistanceThreshold,
|
||||
MainLongpressSourceModel
|
||||
} from './isolatedPathSpecs.js';
|
||||
|
||||
describe("MatcherSelector", function () {
|
||||
beforeEach(function() {
|
||||
this.fakeClock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.fakeClock.restore();
|
||||
});
|
||||
|
||||
describe("Single-source", function() {
|
||||
describe("First stage", function() {
|
||||
it("Longpress (in isolation)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.wait(1000, 50);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: false },
|
||||
specSet: [LongpressModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'chain', item: null, next: 'subkeyselect'}});
|
||||
assert.deepEqual(selection.matcher.model, LongpressModel);
|
||||
assert.isFalse(sources[0].path.isComplete);
|
||||
|
||||
// Allow the rest of the simulation to play out; it's easy cleanup that way.
|
||||
await completion;
|
||||
});
|
||||
|
||||
it("Longpress (with other possibilities)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.wait(1000, 50);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: false },
|
||||
specSet: [LongpressModel, MultitapModel, SimpleTapModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'chain', item: null, next: 'subkeyselect'}});
|
||||
assert.deepEqual(selection.matcher.model, LongpressModel);
|
||||
assert.isFalse(sources[0].path.isComplete);
|
||||
|
||||
// Allow the rest of the simulation to play out; it's easy cleanup that way.
|
||||
await completion;
|
||||
});
|
||||
|
||||
it("Longpress reject (due to path) -> reset request (in isolation)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.move(90, LongpressDistanceThreshold + 2, LongpressModel.contacts[0].model.timer.duration, 20);
|
||||
turtle.wait(1000, 50);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: false },
|
||||
specSet: [LongpressModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
const rejectionStub = sinon.fake();
|
||||
selector.on('rejectionwithaction', rejectionStub);
|
||||
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_PENDING);
|
||||
// It finished simulating without a match. (Note: we don't terminate the simulated
|
||||
// touchpath - `terminate: false`.)
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_RESOLVED);
|
||||
assert.isTrue(rejectionStub.calledOnce);
|
||||
|
||||
const rejectionData = rejectionStub.firstCall.args as [ MatcherSelection<string>, (model: GestureModel<string>) => void ];
|
||||
assert.equal(rejectionData[0].matcher.model.id, 'longpress');
|
||||
assert.equal(rejectionData[0].result.matched, false);
|
||||
assert.deepEqual(rejectionData[0].result.action, { type: 'optional-chain', allowNext: 'longpress', item: null});
|
||||
|
||||
// ... we technically already have it, but this _is_ a convenient pattern to maintain for
|
||||
// consistency among all this suite's tests.
|
||||
await completion;
|
||||
});
|
||||
|
||||
it("Longpress rejection replacement (in isolation)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.move(90, LongpressDistanceThreshold + 2, LongpressModel.contacts[0].model.timer.duration - 2, 20);
|
||||
turtle.hoveredItem = 'b';
|
||||
turtle.wait(1000, 50);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: false },
|
||||
specSet: [LongpressModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
let rejectionCounter = 0;
|
||||
selector.on('rejectionwithaction', (selection, replaceModelWith) => {
|
||||
assert.equal(selection.matcher.model.id, 'longpress');
|
||||
|
||||
rejectionCounter++;
|
||||
// Just... restart the model.
|
||||
replaceModelWith(LongpressModel);
|
||||
});
|
||||
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
// It finished simulating without a match. (Note: we don't terminate the simulated
|
||||
// touchpath - `terminate: false`.)
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'chain', item: null, next: 'subkeyselect'}});
|
||||
assert.deepEqual(selection.matcher.model, LongpressModel);
|
||||
|
||||
// Original base item was 'a'; 'b' proves that a reset occurred by the point of the 'item' change.
|
||||
assert.equal(selection.matcher.baseItem, 'b');
|
||||
assert.isFalse(sources[0].path.isComplete);
|
||||
|
||||
// One for path distance before the longpress timer completed, then one for change of current path 'item'.
|
||||
// (from 'a' to 'b')
|
||||
assert.equal(rejectionCounter, 2);
|
||||
|
||||
await completion;
|
||||
});
|
||||
|
||||
it("Longpress rejection replacement (with other possibilities)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.move(90, LongpressDistanceThreshold + 2, LongpressModel.contacts[0].model.timer.duration - 2, 20);
|
||||
turtle.hoveredItem = 'b';
|
||||
turtle.wait(1000, 50);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: false },
|
||||
// Current problem: adding the extra models leads to rejection of all?
|
||||
specSet: [LongpressModel, MultitapModel, SimpleTapModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
selector.on('rejectionwithaction', (selection, replaceModelWith) => {
|
||||
if(selection.matcher.model.id == 'longpress') {
|
||||
replaceModelWith(LongpressModel);
|
||||
} else if(selection.matcher.model.id == 'simple-tap') {
|
||||
replaceModelWith(SimpleTapModel);
|
||||
} else {
|
||||
assert.fail();
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
// It finished simulating without a match. (Note: we don't terminate the simulated
|
||||
// touchpath - `terminate: false`.)
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'chain', item: null, next: 'subkeyselect'}});
|
||||
assert.deepEqual(selection.matcher.model, LongpressModel);
|
||||
|
||||
// Original base item was 'a'; 'b' proves that a reset occurred by the point of the 'item' change.
|
||||
assert.equal(selection.matcher.baseItem, 'b');
|
||||
assert.isFalse(sources[0].path.isComplete);
|
||||
|
||||
await completion;
|
||||
});
|
||||
|
||||
// roaming touch: relatively long + slow move; expect longpress reset w/ matched: false
|
||||
// (not in isolation)
|
||||
|
||||
it("Simple Tap (single source)", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.wait(100, 5);
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: true },
|
||||
specSet: [LongpressModel, MultitapModel, SimpleTapModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
let completion = executor();
|
||||
const selector = await selectorPromise;
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
// So, the terminate signal didn't complete the selection?
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'optional-chain', item: 'a', allowNext: 'multitap' }});
|
||||
assert.deepEqual(selection.matcher.model, SimpleTapModel);
|
||||
assert.isTrue(sources[0].path.isComplete);
|
||||
|
||||
// Allow the rest of the simulation to play out; it's easy cleanup that way.
|
||||
await completion;
|
||||
});
|
||||
|
||||
it("Simple Tap (single source) with reset", async function() {
|
||||
const turtle = new TouchpathTurtle({
|
||||
targetX: 1,
|
||||
targetY: 1,
|
||||
t: 100,
|
||||
item: 'a'
|
||||
});
|
||||
turtle.wait(40, 5);
|
||||
turtle.move(90, 2, 20, 1);
|
||||
turtle.hoveredItem = 'b';
|
||||
turtle.wait(40, 5);
|
||||
|
||||
turtle.commitPending();
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise,
|
||||
executor
|
||||
} = simulateMultiSourceSelectorInput([
|
||||
{
|
||||
sequence: { type: 'sequence', samples: turtle.path, terminate: true },
|
||||
specSet: [LongpressModel, MultitapModel, SimpleTapModel]
|
||||
}
|
||||
], this.fakeClock);
|
||||
|
||||
let completion = executor();
|
||||
let resets = 0;
|
||||
const selector = await selectorPromise;
|
||||
selector.on('rejectionwithaction', (selection, replaceModelWith) => {
|
||||
if(selection.matcher.model.id == 'longpress') {
|
||||
replaceModelWith(LongpressModel);
|
||||
} else if(selection.matcher.model.id == 'simple-tap') {
|
||||
resets++;
|
||||
replaceModelWith(SimpleTapModel);
|
||||
} else {
|
||||
assert.fail();
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.race([completion, selectionPromises[0]]);
|
||||
|
||||
// So, the terminate signal didn't complete the selection?
|
||||
assert.equal(await promiseStatus(selectionPromises[0]), PromiseStatuses.PROMISE_RESOLVED);
|
||||
assert.equal(await promiseStatus(completion), PromiseStatusModule.PROMISE_PENDING);
|
||||
|
||||
const selection = await selectionPromises[0];
|
||||
|
||||
assert.deepEqual(selection.result, {matched: true, action: { type: 'optional-chain', item: 'b', allowNext: 'multitap' }});
|
||||
assert.deepEqual(selection.matcher.model, SimpleTapModel);
|
||||
assert.isTrue(sources[0].path.isComplete);
|
||||
assert.isAtLeast(resets, 1);
|
||||
|
||||
// Allow the rest of the simulation to play out; it's easy cleanup that way.
|
||||
await completion;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,8 @@ import { ManagedPromise, timedPromise } from '@keymanapp/web-utils';
|
|||
import { GestureSourceSubview } from '../../../build/obj/headless/gestureSource.js';
|
||||
|
||||
type GestureMatcher<Type> = gestures.matchers.GestureMatcher<Type>;
|
||||
type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
|
||||
type MatcherSelector<Type> = gestures.matchers.MatcherSelector<Type>;
|
||||
type GestureModel<Type> = gestures.specs.GestureModel<Type>;
|
||||
|
||||
interface SimSpecSequence<Type> {
|
||||
|
|
@ -328,4 +330,71 @@ export function simulateMultiSourceMatcherInput<Type>(
|
|||
modelMatcherPromise: testObjPromise,
|
||||
executor
|
||||
};
|
||||
}
|
||||
|
||||
type MatcherSelectorInput<Type> = {
|
||||
sequence: (SimSpecTimer<Type> | SimSpecSequence<Type>),
|
||||
specSet: GestureModel<Type>[]
|
||||
}[];
|
||||
|
||||
export function simulateMultiSourceSelectorInput<Type>(
|
||||
input: MatcherSelectorInput<Type>,
|
||||
fakeClock: sinon.SinonFakeTimers
|
||||
): {
|
||||
sources: GestureSource<Type>[],
|
||||
selectionPromises: Promise<MatcherSelection<Type>>[],
|
||||
selectorPromise: Promise<MatcherSelector<Type>>,
|
||||
executor: () => Promise<void>
|
||||
} {
|
||||
let inputClone = [].concat(input);
|
||||
|
||||
// We NEED the sequences specified to be in chronological order of their start.
|
||||
// We'll just check if it's done properly out-of-the-gate - by sorting a clone, then comparing.
|
||||
// We shouldn't actually mutate / correct this b/c of the returned `selectorPromises` field.
|
||||
inputClone.sort((a, b) => {
|
||||
if(a.sequence.type == "timer") {
|
||||
return -1;
|
||||
} else if(b.sequence.type == "timer") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.sequence.samples[0].t - b.sequence.samples[0].t
|
||||
}
|
||||
});
|
||||
|
||||
for(let i=0; i < input.length; i++) {
|
||||
if(input[i] != inputClone[i]) {
|
||||
throw new Error("The specified input data should be in chronological order based on its start position.");
|
||||
}
|
||||
}
|
||||
|
||||
let indexSeed = 0;
|
||||
const selectionPromises: Promise<MatcherSelection<Type>>[] = [];
|
||||
|
||||
const config: SimulationConfig<MatcherSelector<Type>, Type> = {
|
||||
construction: (source) => {
|
||||
const selector = new gestures.matchers.MatcherSelector<Type>();
|
||||
|
||||
// TS can't resolve the two-typed parameter to two separate overloads of the same method, it seems.
|
||||
selectionPromises.push(selector.matchGesture(source as any, input[indexSeed++].specSet));
|
||||
|
||||
return selector;
|
||||
},
|
||||
addSource: (obj, source) => {
|
||||
obj.matchGesture(source, input[indexSeed++].specSet);
|
||||
},
|
||||
update: () => {}
|
||||
}
|
||||
|
||||
const {
|
||||
sources,
|
||||
testObjPromise,
|
||||
executor
|
||||
} = simulateMultiSourceInput(config, input.map((entry) => entry.sequence), fakeClock);
|
||||
|
||||
return {
|
||||
sources,
|
||||
selectionPromises,
|
||||
selectorPromise: testObjPromise,
|
||||
executor
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue