From ba568bef404b48dcf555a767916cae397d2c8fa0 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 1 Sep 2022 12:36:22 +0700 Subject: [PATCH] feat(web): first segmentation test based on test-recording --- ...cSegmentation.js => basicSegmentations.js} | 5 - .../auto/headless/recordedSegmentations.js | 117 ++++++++ .../json/segmentation/simple_ne_move.json | 280 ++++++++++++++++++ 3 files changed, 397 insertions(+), 5 deletions(-) rename common/web/gesture-recognizer/src/test/auto/headless/{basicSegmentation.js => basicSegmentations.js} (98%) create mode 100644 common/web/gesture-recognizer/src/test/auto/headless/recordedSegmentations.js create mode 100644 common/web/gesture-recognizer/src/test/resources/json/segmentation/simple_ne_move.json diff --git a/common/web/gesture-recognizer/src/test/auto/headless/basicSegmentation.js b/common/web/gesture-recognizer/src/test/auto/headless/basicSegmentations.js similarity index 98% rename from common/web/gesture-recognizer/src/test/auto/headless/basicSegmentation.js rename to common/web/gesture-recognizer/src/test/auto/headless/basicSegmentations.js index 8840503422..b964c3d2bf 100644 --- a/common/web/gesture-recognizer/src/test/auto/headless/basicSegmentation.js +++ b/common/web/gesture-recognizer/src/test/auto/headless/basicSegmentations.js @@ -1,8 +1,6 @@ const assert = require('chai').assert; const sinon = require('sinon'); -const fs = require('fs'); - const PromiseStatusModule = require('promise-status-async'); const promiseStatus = PromiseStatusModule.promiseStatus; const PromiseStatuses = PromiseStatusModule.PromiseStatuses; @@ -14,9 +12,6 @@ const PathSegmenter = com.keyman.osk.PathSegmenter; const timedPromise = require('../../resources/timedPromise.js'); describe("Segmentation", function() { - // // File paths need to be from the package's / module's root folder - // let testJSONtext = fs.readFileSync('src/test/resources/json/canaryRecording.json'); - describe("Single-sample 'sequence'", function() { it("expected segment types", function() { let spy = sinon.fake(); diff --git a/common/web/gesture-recognizer/src/test/auto/headless/recordedSegmentations.js b/common/web/gesture-recognizer/src/test/auto/headless/recordedSegmentations.js new file mode 100644 index 0000000000..106569b796 --- /dev/null +++ b/common/web/gesture-recognizer/src/test/auto/headless/recordedSegmentations.js @@ -0,0 +1,117 @@ +const assert = require('chai').assert; +const sinon = require('sinon'); + +const fs = require('fs'); + +const PromiseStatusModule = require('promise-status-async'); +const promiseStatus = PromiseStatusModule.promiseStatus; +const PromiseStatuses = PromiseStatusModule.PromiseStatuses; + +const GestureRecognizer = require('../../../../build/index.js'); +const com = GestureRecognizer.com; +const PathSegmenter = com.keyman.osk.PathSegmenter; + +const timedPromise = require('../../resources/timedPromise.js'); + +const SEGMENT_TEST_JSON_FOLDER = 'src/test/resources/json/segmentation'; + +describe("Segmentation", function() { + describe("Recorded sequence tests", function() { + beforeEach(function() { + this.fakeClock = sinon.useFakeTimers(); + }) + + afterEach(function() { + // NOTE: for debugging investigations, it may be necessary to use .only on + // the test under investigation and to disable the `this.fakeClock.restore()` line. + // + // Tests tend to timeout when interactively debugging, and having unmocked timers + // suddenly restored during investigation can cause some very confusing behavior. + this.fakeClock.restore(); + }) + + it("simple_ne_move.json", async function() { + // NOTE: this recording's final 'hold' segment is somewhat tightly attuned to the DEFAULT_CONFIG + // hold time setting. Changing default values there may necessitate a hand-edit tweak to the + // test recording's data in order for this test to continue passing as-is. + + let testJSONtext = fs.readFileSync(`${SEGMENT_TEST_JSON_FOLDER}/simple_ne_move.json`); + let jsonObj = JSON.parse(testJSONtext); + + // Prepare some of the basic setup. + let spy = sinon.fake(); + + const segmenter = new PathSegmenter(PathSegmenter.DEFAULT_CONFIG, spy); + + // Next: drill down to the relevant part(s). + const sourceTrackedPath = jsonObj.inputs[0].touchpoints[0].path; + + const sourceSamples = sourceTrackedPath.coords; + const originalSegments = sourceTrackedPath.segments; + const lastSegment = originalSegments[originalSegments.length-1]; + + // Build promises designed to reproduce the events at the correct times. + const samplePromises = sourceSamples.map((sample) => { + return timedPromise(() => { + segmenter.add(sample); + }, sample.t - sourceSamples[0].t); + }); + + // Extend any Promises here if relevant + + // Segmentation is then ended via a followup event. + // The source recording does not save an input for the 'touchend' event - but + // we can infer the correct time from the final segment. + + const segmentationEndPromise = timedPromise(() => { + segmenter.close(); + }, lastSegment.lastCoord.t - sourceSamples[0].t); + + // Wrap it all together with a nice little bow. + const finalPromise = Promise.all([...samplePromises, segmentationEndPromise]).catch((reason) => { + // Because we use a `setInterval` internally, we need cleanup if things go wrong. + segmenter.close(); + throw reason; + }); + + await this.fakeClock.runAllAsync(); + + // Make sure this Promise also gets to complete. + await finalPromise; + + // Any post-sequence tests to run. + const originalSegmentTypeSequence = originalSegments.map((segment) => segment.type); + + const reproedSegments = spy.getCalls().map((call) => call.args[0]); + const reproedSegmentTypeSequence = reproedSegments.map((segment) => segment.type); + + assert.sameOrderedMembers(reproedSegmentTypeSequence, originalSegmentTypeSequence); + + for(let segment of reproedSegments) { + assert.isTrue(await promiseStatus(segment.whenRecognized) == PromiseStatuses.PROMISE_RESOLVED); + assert.isTrue(await promiseStatus(segment.whenResolved) == PromiseStatuses.PROMISE_RESOLVED); + } + + // Complete our deserialization of the original segment in question - needed for + // clearer, more direct comparisons. + const holdSegmentOriginal = new com.keyman.osk.Segment(originalSegments[1]); + const holdSegmentRepro = reproedSegments[1]; + + assert.equal(holdSegmentRepro.type, 'hold'); + assert.isAtLeast(holdSegmentRepro.duration, holdSegmentOriginal.duration - PathSegmenter.DEFAULT_CONFIG.holdMinimumDuration); + + // Complete our deserialization of the original segment in question - needed for + // clearer, more direct comparisons. + const moveSegmentOriginal = new com.keyman.osk.Segment(originalSegments[2]); + const moveSegmentRepro = reproedSegments[2]; + + assert.equal(moveSegmentRepro.type, 'move'); + assert.isAtLeast(moveSegmentRepro.duration, moveSegmentOriginal.duration - PathSegmenter.DEFAULT_CONFIG.holdMinimumDuration); + + // Move-specific checks + assert.equal(moveSegmentRepro.direction, moveSegmentOriginal.direction); + assert.isAtLeast(moveSegmentRepro.distance, moveSegmentOriginal.distance - 2 * PathSegmenter.DEFAULT_CONFIG.holdMoveTolerance); + assert.isAtLeast(moveSegmentRepro.peakSpeed, 0.9 * moveSegmentOriginal.peakSpeed); + }); + }); +}); \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/test/resources/json/segmentation/simple_ne_move.json b/common/web/gesture-recognizer/src/test/resources/json/segmentation/simple_ne_move.json new file mode 100644 index 0000000000..9276eddd83 --- /dev/null +++ b/common/web/gesture-recognizer/src/test/resources/json/segmentation/simple_ne_move.json @@ -0,0 +1,280 @@ +{ + "inputs": [ + { + "touchpoints": [ + { + "isFromTouch": false, + "path": { + "coords": [ + { + "targetX": 52, + "targetY": 134, + "t": 10174 + }, + { + "targetX": 52, + "targetY": 133, + "t": 10373 + }, + { + "targetX": 52, + "targetY": 132, + "t": 10386 + }, + { + "targetX": 53, + "targetY": 131, + "t": 10403 + }, + { + "targetX": 53, + "targetY": 130, + "t": 10417 + }, + { + "targetX": 56, + "targetY": 126, + "t": 10433 + }, + { + "targetX": 59, + "targetY": 121, + "t": 10450 + }, + { + "targetX": 61, + "targetY": 118, + "t": 10467 + }, + { + "targetX": 64, + "targetY": 114, + "t": 10484 + }, + { + "targetX": 68, + "targetY": 108, + "t": 10500 + }, + { + "targetX": 73, + "targetY": 103, + "t": 10517 + }, + { + "targetX": 77, + "targetY": 98, + "t": 10534 + }, + { + "targetX": 80, + "targetY": 95, + "t": 10550 + }, + { + "targetX": 82, + "targetY": 91, + "t": 10567 + }, + { + "targetX": 85, + "targetY": 89, + "t": 10583 + }, + { + "targetX": 87, + "targetY": 86, + "t": 10601 + }, + { + "targetX": 89, + "targetY": 84, + "t": 10617 + }, + { + "targetX": 91, + "targetY": 82, + "t": 10633 + }, + { + "targetX": 93, + "targetY": 80, + "t": 10650 + }, + { + "targetX": 94, + "targetY": 77, + "t": 10667 + }, + { + "targetX": 97, + "targetY": 73, + "t": 10683 + }, + { + "targetX": 99, + "targetY": 70, + "t": 10700 + }, + { + "targetX": 101, + "targetY": 68, + "t": 10717 + }, + { + "targetX": 102, + "targetY": 65, + "t": 10734 + }, + { + "targetX": 104, + "targetY": 63, + "t": 10750 + }, + { + "targetX": 106, + "targetY": 60, + "t": 10766 + }, + { + "targetX": 107, + "targetY": 58, + "t": 10784 + }, + { + "targetX": 108, + "targetY": 57, + "t": 10800 + }, + { + "targetX": 109, + "targetY": 55, + "t": 10816 + }, + { + "targetX": 110, + "targetY": 53, + "t": 10833 + }, + { + "targetX": 110, + "targetY": 52, + "t": 10851 + }, + { + "targetX": 111, + "targetY": 52, + "t": 10869 + }, + { + "targetX": 111, + "targetY": 52, + "t": 10951 + } + ], + "segments": [ + { + "type": "start", + "duration": 0, + "speed": 0, + "distance": 0, + "peakSpeed": 0, + "initialCoord": { + "targetX": 52, + "targetY": 134, + "t": 10174 + }, + "lastCoord": { + "targetX": 52, + "targetY": 134, + "t": 10174 + } + }, + { + "type": "hold", + "duration": 203, + "cardinalDirection": "n", + "speed": 0.01696751286262412, + "distance": 4.123105625617661, + "angle": 0.24497866312686423, + "peakSpeed": 0, + "initialCoord": { + "targetX": 52, + "targetY": 134, + "t": 10174 + }, + "lastCoord": { + "targetX": 53, + "targetY": 130, + "t": 10377 + } + }, + { + "type": "move", + "duration": 574, + "cardinalDirection": "ne", + "speed": 0.17599268299546728, + "distance": 101.01980003939822, + "angle": 0.6237017162268916, + "peakSpeed": 0.5202346361292709, + "initialCoord": { + "targetX": 52, + "targetY": 134, + "t": 10337 + }, + "lastCoord": { + "targetX": 111, + "targetY": 52, + "t": 10911 + } + }, + { + "type": "hold", + "duration": 102, + "cardinalDirection": "e", + "speed": 0.00980392156862745, + "distance": 1, + "angle": 1.5707963267948966, + "peakSpeed": 0.041666666666666796, + "initialCoord": { + "targetX": 110, + "targetY": 52, + "t": 10851 + }, + "lastCoord": { + "targetX": 111, + "targetY": 52, + "t": 10953 + } + }, + { + "type": "end", + "duration": 0, + "speed": 0, + "distance": 0, + "peakSpeed": 0, + "initialCoord": { + "targetX": 111, + "targetY": 52, + "t": 10953 + }, + "lastCoord": { + "targetX": 111, + "targetY": 52, + "t": 10953 + } + } + ], + "wasCancelled": false + } + } + ] + } + ], + "config": { + "deviceStyle": "screen2", + "roamingStyle": "bounds1", + "receiverStyle": "full", + "safeZoneStyle": "safe-loose" + } +} \ No newline at end of file