From 654d5611deff7b82a6ce2ebc01877da515f1c72f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 29 Aug 2023 12:45:53 +0700 Subject: [PATCH] chore(web): prepping for gesture-sequence tests --- .../configuration/gestureConfiguration.ts | 11 --- .../gestures/matchers/gestureSequence.ts | 33 ++----- .../headless/gestures/matchers/index.ts | 1 + .../gestures/specs/gestureModelDefs.ts | 37 ++++++++ .../engine/headless/gestures/specs/index.ts | 1 + .../engine/headless/touchpointCoordinator.ts | 4 +- .../gesture-recognizer/src/engine/index.ts | 1 + .../headless/gestures/gestureSequence.spec.ts | 89 +++++++++++++++++++ 8 files changed, 137 insertions(+), 40 deletions(-) delete mode 100644 common/web/gesture-recognizer/src/engine/configuration/gestureConfiguration.ts create mode 100644 common/web/gesture-recognizer/src/engine/headless/gestures/specs/gestureModelDefs.ts diff --git a/common/web/gesture-recognizer/src/engine/configuration/gestureConfiguration.ts b/common/web/gesture-recognizer/src/engine/configuration/gestureConfiguration.ts deleted file mode 100644 index c5ea40efdfb..00000000000 --- a/common/web/gesture-recognizer/src/engine/configuration/gestureConfiguration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as gestures from "../headless/gestures/index.js"; - -// Prototype spec for the main gesture & gesture-set definitions. -// A work in-progress. Should probably land somewhere within headless/gestures/specs/. -// ... with the following two functions, as well. -export interface GestureModelDefs { - gestures: gestures.specs.GestureModel[], - sets: { - default: string[], - } & Record; -} \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts index 79837ecf9ea..03c0a162934 100644 --- a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/gestureSequence.ts @@ -1,6 +1,6 @@ import EventEmitter from "eventemitter3"; -import { GestureModelDefs } from "../../../configuration/gestureConfiguration.js"; +import { GestureModelDefs, getGestureModel, getGestureModelSet } from "../specs/gestureModelDefs.js"; import { GestureSource, GestureSourceSubview } from "../../gestureSource.js"; import { GestureMatcher, MatchResult, PredecessorMatch } from "./gestureMatcher.js"; import { GestureModel } from "../specs/gestureModel.js"; @@ -55,7 +55,6 @@ interface EventMap { stageReport: GestureStageReport, changeConfiguration: (configStackCommand: PushConfig | PopConfig) => void ) => void; - cancel: () => void; complete: () => void; } @@ -106,6 +105,11 @@ export class GestureSequence extends EventEmitter> { const matchReport = new GestureStageReport(selection); this.stageReports.push(matchReport); + if(!selection.result.matched && selection.result.action.type == 'complete') { + this.emit('complete'); + return; + } + // Raise the event, providing a functor that allows the listener to specify an alt config for the next stage. // Example case: longpress => subkey selection - the subkey menu has different boundary conditions. this.emit('stage', matchReport, (command) => { @@ -220,31 +224,6 @@ export class GestureSequence extends EventEmitter> { }; } -export function getGestureModel(defs: GestureModelDefs, id: string): GestureModel { - const result = defs.gestures.find((spec) => spec.id == id); - if(!result) { - throw new Error(`Could not find spec for gesture with id '${id}'`); - } - - return result; -} - -export function getGestureModelSet(defs: GestureModelDefs, id: string): GestureModel[] { - let idSet = defs.sets[id]; - if(!idSet) { - throw new Error(`Could not find a defined gesture-set with id '${id}'`); - } - - const set = defs.gestures.filter((spec) => !!idSet.find((id) => spec.id == id)); - const missing = idSet.filter((id) => !set.find((spec) => spec.id == id)); - - if(missing.length > 0) { - throw new Error(`Set '${id}' cannot find definitions for gestures with ids ${missing}`); - } - - return set; -} - interface ProcessedGestureAction { nextModels: GestureModel[], sources: GestureSource[], diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/index.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/index.ts index e1cfadc2bbf..b832880e1c0 100644 --- a/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/index.ts +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/matchers/index.ts @@ -1,3 +1,4 @@ export { GestureMatcher } from './gestureMatcher.js'; +export { GestureSequence } from './gestureSequence.js'; export { MatcherSelection, MatcherSelector } from './matcherSelector.js'; export { PathMatcher } from './pathMatcher.js'; \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/specs/gestureModelDefs.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/specs/gestureModelDefs.ts new file mode 100644 index 00000000000..7c1fdc56121 --- /dev/null +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/specs/gestureModelDefs.ts @@ -0,0 +1,37 @@ +import * as gestures from "../index.js"; + +// Prototype spec for the main gesture & gesture-set definitions. +// A work in-progress. Should probably land somewhere within headless/gestures/specs/. +// ... with the following two functions, as well. +export interface GestureModelDefs { + gestures: gestures.specs.GestureModel[], + sets: { + default: string[], + } & Record; +} + + +export function getGestureModel(defs: GestureModelDefs, id: string): gestures.specs.GestureModel { + const result = defs.gestures.find((spec) => spec.id == id); + if(!result) { + throw new Error(`Could not find spec for gesture with id '${id}'`); + } + + return result; +} + +export function getGestureModelSet(defs: GestureModelDefs, id: string): gestures.specs.GestureModel[] { + let idSet = defs.sets[id]; + if(!idSet) { + throw new Error(`Could not find a defined gesture-set with id '${id}'`); + } + + const set = defs.gestures.filter((spec) => !!idSet.find((id) => spec.id == id)); + const missing = idSet.filter((id) => !set.find((spec) => spec.id == id)); + + if(missing.length > 0) { + throw new Error(`Set '${id}' cannot find definitions for gestures with ids ${missing}`); + } + + return set; +} \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/engine/headless/gestures/specs/index.ts b/common/web/gesture-recognizer/src/engine/headless/gestures/specs/index.ts index d432da88d37..d568d97f44c 100644 --- a/common/web/gesture-recognizer/src/engine/headless/gestures/specs/index.ts +++ b/common/web/gesture-recognizer/src/engine/headless/gestures/specs/index.ts @@ -1,3 +1,4 @@ export * from './contactModel.js'; export * from './gestureModel.js'; +export * from './gestureModelDefs.js'; export * from './pathModel.js'; \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/engine/headless/touchpointCoordinator.ts b/common/web/gesture-recognizer/src/engine/headless/touchpointCoordinator.ts index 77f0be00c29..3156374fcbd 100644 --- a/common/web/gesture-recognizer/src/engine/headless/touchpointCoordinator.ts +++ b/common/web/gesture-recognizer/src/engine/headless/touchpointCoordinator.ts @@ -2,8 +2,8 @@ import EventEmitter from "eventemitter3"; import { InputEngineBase } from "./inputEngineBase.js"; import { GestureSource, GestureSourceSubview } from "./gestureSource.js"; import { MatcherSelector } from "./gestures/matchers/matcherSelector.js"; -import { GestureSequence, getGestureModelSet } from "./gestures/matchers/gestureSequence.js"; -import { GestureModelDefs } from "../configuration/gestureConfiguration.js"; +import { GestureSequence } from "./gestures/matchers/gestureSequence.js"; +import { GestureModelDefs, getGestureModelSet } from "./gestures/specs/gestureModelDefs.js"; interface EventMap { /** diff --git a/common/web/gesture-recognizer/src/engine/index.ts b/common/web/gesture-recognizer/src/engine/index.ts index 34756219ebd..7203edcbb5e 100644 --- a/common/web/gesture-recognizer/src/engine/index.ts +++ b/common/web/gesture-recognizer/src/engine/index.ts @@ -1,5 +1,6 @@ export { ConstructingSegment } from './headless/subsegmentation/constructingSegment.js'; export { CumulativePathStats } from './headless/cumulativePathStats.js'; +export { GestureModelDefs } from './headless/gestures/specs/gestureModelDefs.js'; export { GestureRecognizer } from "./gestureRecognizer.js"; export { GestureRecognizerConfiguration } from "./configuration/gestureRecognizerConfiguration.js"; export { InputEngineBase } from "./headless/inputEngineBase.js"; diff --git a/common/web/gesture-recognizer/src/test/auto/headless/gestures/gestureSequence.spec.ts b/common/web/gesture-recognizer/src/test/auto/headless/gestures/gestureSequence.spec.ts index e69de29bb2d..b4f62857351 100644 --- a/common/web/gesture-recognizer/src/test/auto/headless/gestures/gestureSequence.spec.ts +++ b/common/web/gesture-recognizer/src/test/auto/headless/gestures/gestureSequence.spec.ts @@ -0,0 +1,89 @@ +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 { GestureModelDefs, GestureSource, gestures, InputSample } from '@keymanapp/gesture-recognizer'; + +import { TouchpathTurtle } from '#tools'; +import { ManagedPromise, timedPromise } from '@keymanapp/web-utils'; + +import { simulateMultiSourceMatcherInput } from "../../../resources/simulateMultiSourceInput.js"; + +import { + LongpressModel, + MultitapModel, + SimpleTapModel, + SubkeySelectModel +} from './isolatedGestureSpecs.js'; + +import { + LongpressDistanceThreshold, + MainLongpressSourceModel +} from './isolatedPathSpecs.js'; + +const TestGestureModelDefinitions: GestureModelDefs = { + gestures: [ + LongpressModel, + MultitapModel, + SimpleTapModel, + SubkeySelectModel + ], + // TODO: modipress mode + sets: { + default: [LongpressModel.id, SimpleTapModel.id] + } +} + +// TODO(?): right, simulation. Again. Yaaaaay. +// - Fortunately, the _start_ can just use selection-sim semantics; the GestureSequence +// constructor takes in an existing Selector & its selection, after all. +// - in fact... that should be 100% fine, right? There's only ever the one selector! +// - the issue: we do want to 'select' early, before later-stage timers are all run. + +// Also to-test: some of the component functions outside of the GestureSequence class. +// They're fairly simple to do, right? +// - processGestureAction, in particular. + +// ALSO possible to test: GestureSource (with some of the 'fun', new wrinkles.) + +// Possible to test, though separate file - the gesture-model / gesture-model-set +// retrieval funcs. + +// Later, in a different file: testing TouchpointCoordinator's integration with this. + +describe("GestureSequence", function() { + beforeEach(function() { + this.fakeClock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.fakeClock.restore(); + }); + + // TODO: author tests for (at least) the following + // A single, simple-tap + // Two+ separate simple-taps (overlapping time interval) + // - could be mitigated with a special 'flag' on the gesture-model, perhaps? + // - would be 'nice', conceptually, for consumers... but I don't think it's worth prioritizing + // at the moment. Got enough else to deal with for now. + // - but the Sequence DOES hold a ref to the coordinator object, so... it technically _could_ + // spin off a separate sequence? + // Longpress: hold -> subkey-select + // simple-tap => multi-tap (x2) + // Modipress + // Android longpress delegation + // + // Defer: flick test - confirm => execute + // + // .on('complete') may be checked to validate if any further match attempts will be possible + // based on the condition; it may be worth 'extending' tests (via mocked timer) to + // double-check such scenarios. + + it('first sequence test', () => { + // simulateSelectorInput should be sufficient for sequence emulation; just capture + // the first selection once kick-started, then add the "fun" hooks for the rest of the test. + }); +}); \ No newline at end of file