fix(web): identified sequence-repro issues

This commit is contained in:
Joshua A. Horton 2022-07-19 14:49:31 +07:00
parent 5cbe959bc0
commit bacfe6d507
9 changed files with 456 additions and 65 deletions

View file

@ -77,23 +77,27 @@ namespace com.keyman.osk {
sequenceWrapper.signalUpdate(sample);
}
onInputMoveCancel(identifier: number) {
onInputMoveCancel(identifier: number, sample: InputSample) {
let sequenceWrapper = this.getSequenceWrapperWithId(identifier);
if(!sequenceWrapper) {
return;
}
sequenceWrapper.item.addSample(sample);
sequenceWrapper.signalUpdate(sample);
sequenceWrapper.cancel();
}
onInputEnd(identifier: number, sample: InputSample) {
onInputEnd(identifier: number) {
let sequenceWrapper = this.getSequenceWrapperWithId(identifier);
if(!sequenceWrapper) {
return;
}
// We do not add a sample here because any 'end' event immediately follows a
// 'move' if it occurred simultaneously.
sequenceWrapper.end();
}
}

View file

@ -118,7 +118,7 @@ namespace com.keyman.osk {
if(!event.buttons) {
if(this.hasActiveClick) {
this.hasActiveClick = false;
this.onInputMoveCancel(this.activeIdentifier);
this.onInputMoveCancel(this.activeIdentifier, sample);
}
return;
}
@ -128,7 +128,7 @@ namespace com.keyman.osk {
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, this.config, this.disabledSafeBounds)) {
this.onInputMove(this.activeIdentifier, sample);
} else {
this.onInputMoveCancel(this.activeIdentifier);
this.onInputMoveCancel(this.activeIdentifier, sample);
}
}
@ -141,8 +141,7 @@ namespace com.keyman.osk {
this.hasActiveClick = false;
}
const sample = this.buildSampleFromEvent(event);
this.onInputEnd(this.activeIdentifier, sample);
this.onInputEnd(this.activeIdentifier);
}
}
}

View file

@ -1,46 +0,0 @@
var assert = chai.assert;
var expect = chai.expect;
describe("Layer one - DOM -> InputSequence", function() {
this.timeout(testconfig.timeouts.standard);
before(function() {
fixture.setBase('');
});
beforeEach(function(done) {
fixture.load('host-fixture.html');
this.controller = new Testing.HostFixtureLayoutController();
this.controller.connect().then(() => done());
});
afterEach(function() {
this.controller.destroy();
fixture.cleanup();
});
it('First true unit test', function() {
let playbackEngine = new Testing.InputSequenceSimulator(this.controller);
let testObj = __json__.canaryRecording; // See src/test/resources/json/canaryRecording.json
let result = playbackEngine.replay(testObj);
assert.equal(testObj.set.length, result.set.length);
// Removes the timestamp element; we know that this component won't match, as the playback engine
// doesn't care about it.
let sampleCleaner = (sample) => { return {targetX: sample.targetX, targetY: sample.targetY} };
// Returns just the observed, cleaned samples for a sequence object. The recorded coordinates
// should match perfectly.
let seqCleaner = (seq) => { return seq.sequence.samples.map(sampleCleaner) };
let cleanOriginalSet = testObj.set.map(seqCleaner);
let cleanResultSet = result .set.map(seqCleaner);
expect(cleanResultSet).to.deep.equal(cleanOriginalSet);
// The 'terminationEvent' property should match. Any sequence that was "canceled" should still
// cancel; that's a pretty critical detail!
let terminationEventMapper = (seq) => seq.terminationEvent;
expect(result.set.map(terminationEventMapper)).to.deep.equal(testObj.set.map(terminationEventMapper));
});
});

View file

@ -0,0 +1,73 @@
var assert = chai.assert;
var expect = chai.expect;
describe("Layer one - DOM -> InputSequence", function() {
this.timeout(testconfig.timeouts.standard);
before(function() {
fixture.setBase('');
});
beforeEach(function(done) {
fixture.load('host-fixture.html');
this.controller = new Testing.HostFixtureLayoutController();
this.controller.connect().then(() => done());
});
afterEach(function() {
this.controller.destroy();
fixture.cleanup();
fixture.cleanup();
});
describe('recorded input sequences', function() {
// We rely on this function to have the same context as `it` - the test-definition function.
let replayAndCompare = function(testObj) {
// Everything else can be extracted to a common test function... probably.
let playbackEngine = new Testing.InputSequenceSimulator(this.controller);
let result = playbackEngine.replay(testObj);
assert.equal(result.set.length, testObj.set.length);
// Removes the timestamp element; we know that this component won't match, as the playback engine
// doesn't care about it.
let sampleCleaner = (sample) => { return {targetX: sample.targetX, targetY: sample.targetY} };
// Returns just the observed, cleaned samples for a sequence object. The recorded coordinates
// should match perfectly.
let seqCleaner = (seq) => { return seq.sequence.samples.map(sampleCleaner) };
let cleanOriginalSet = testObj.set.map(seqCleaner);
let cleanResultSet = result .set.map(seqCleaner);
expect(cleanResultSet).to.deep.equal(cleanOriginalSet);
// The 'terminationEvent' property should match. Any sequence that was "canceled" should still
// cancel; that's a pretty critical detail!
let terminationEventMapper = (seq) => seq.terminationEvent;
expect(result.set.map(terminationEventMapper)).to.deep.equal(testObj.set.map(terminationEventMapper));
}
// List all relevant fixtures in src/test/resources/json.
let testRecordings = [
'canaryRecording',
'desktopRoamAndReturn',
'mobileSafeZoneCancel',
'mobileProximityApproach',
'embeddedBorderCancel',
'hardBorderCancel',
'popupLongRoamingEnd',
'popupShimCancel'
];
for(let recordingID of testRecordings) {
it(`${recordingID}.json`, function() {
let testObj = __json__[recordingID];
console.log(`expected terminator: ${testObj.set[0].terminationEvent}`)
// 'describe' has a notably different `this` reference than `it`, `before`, etc.
replayAndCompare.call(this, testObj);
});
}
});
});

View file

@ -0,0 +1,288 @@
{
"set": [
{
"sequence": {
"samples": [
{
"targetX": 119,
"targetY": 27,
"t": 670652.6999999881
},
{
"targetX": 119,
"targetY": 26,
"t": 670770.8999999762
},
{
"targetX": 119,
"targetY": 25,
"t": 670783.5
},
{
"targetX": 119,
"targetY": 24,
"t": 670792.5
},
{
"targetX": 119,
"targetY": 24,
"t": 670815.2999999523
},
{
"targetX": 119,
"targetY": 23,
"t": 670825.8999999762
},
{
"targetX": 119,
"targetY": 22,
"t": 670848.1999999881
},
{
"targetX": 119,
"targetY": 21,
"t": 670859.0999999642
},
{
"targetX": 119,
"targetY": 20,
"t": 670875.8999999762
},
{
"targetX": 119,
"targetY": 20,
"t": 670892.6999999881
},
{
"targetX": 119,
"targetY": 19,
"t": 670909.1999999881
},
{
"targetX": 119,
"targetY": 18,
"t": 670926.5
},
{
"targetX": 119,
"targetY": 16,
"t": 670942.5
},
{
"targetX": 118,
"targetY": 15,
"t": 670959.1999999881
},
{
"targetX": 118,
"targetY": 12,
"t": 670975.7999999523
},
{
"targetX": 118,
"targetY": 12,
"t": 670993.1999999881
},
{
"targetX": 118,
"targetY": 10,
"t": 671009.1999999881
},
{
"targetX": 118,
"targetY": 8,
"t": 671025.8999999762
},
{
"targetX": 118,
"targetY": 6,
"t": 671042.5999999642
},
{
"targetX": 118,
"targetY": 5,
"t": 671059.2999999523
},
{
"targetX": 118,
"targetY": 4,
"t": 671075.8999999762
},
{
"targetX": 118,
"targetY": 2,
"t": 671092.5999999642
},
{
"targetX": 118,
"targetY": 0,
"t": 671109.1999999881
},
{
"targetX": 118,
"targetY": 0,
"t": 671125.8999999762
},
{
"targetX": 118,
"targetY": -2,
"t": 671142.5999999642
},
{
"targetX": 118,
"targetY": -3,
"t": 671159.0999999642
},
{
"targetX": 118,
"targetY": -4,
"t": 671175.7999999523
},
{
"targetX": 118,
"targetY": -4,
"t": 671192.5
},
{
"targetX": 118,
"targetY": -5,
"t": 671244.3999999762
},
{
"targetX": 118,
"targetY": -6,
"t": 671260
},
{
"targetX": 119,
"targetY": -7,
"t": 671275.8999999762
},
{
"targetX": 120,
"targetY": -7,
"t": 671326.1999999881
},
{
"targetX": 120,
"targetY": -8,
"t": 671342.5
},
{
"targetX": 120,
"targetY": -8,
"t": 671359.8999999762
},
{
"targetX": 120,
"targetY": -8,
"t": 671376
},
{
"targetX": 121,
"targetY": -8,
"t": 671395.1999999881
},
{
"targetX": 121,
"targetY": -9,
"t": 671420.5999999642
},
{
"targetX": 122,
"targetY": -9,
"t": 671460
},
{
"targetX": 123,
"targetY": -9,
"t": 671475.8999999762
},
{
"targetX": 124,
"targetY": -9,
"t": 671525.8999999762
},
{
"targetX": 125,
"targetY": -8,
"t": 671542.5999999642
},
{
"targetX": 126,
"targetY": -8,
"t": 671559.2999999523
},
{
"targetX": 127,
"targetY": -6,
"t": 671575.7999999523
},
{
"targetX": 128,
"targetY": -4,
"t": 671592.5999999642
},
{
"targetX": 128,
"targetY": -3,
"t": 671609.2999999523
},
{
"targetX": 129,
"targetY": -1,
"t": 671625.8999999762
},
{
"targetX": 129,
"targetY": 0,
"t": 671642.5999999642
},
{
"targetX": 129,
"targetY": 0,
"t": 671659.1999999881
},
{
"targetX": 129,
"targetY": 1,
"t": 671675.8999999762
},
{
"targetX": 130,
"targetY": 1,
"t": 671692.8999999762
},
{
"targetX": 130,
"targetY": 3,
"t": 671709.2999999523
},
{
"targetX": 130,
"targetY": 4,
"t": 671734.8999999762
},
{
"targetX": 130,
"targetY": 4,
"t": 671749.5
},
{
"targetX": 130,
"targetY": 5,
"t": 671793
}
],
"isFromTouch": false
},
"terminationEvent": "end"
}
],
"config": {
"deviceStyle": "screen3",
"roamingStyle": "bounds1",
"receiverStyle": "full",
"safeZoneStyle": "safe-loose"
}
}

View file

@ -0,0 +1,38 @@
{
"set": [
{
"sequence": {
"samples": [
{
"targetX": 226,
"targetY": 38,
"t": 61290
},
{
"targetX": 234,
"targetY": 38,
"t": 61676
},
{
"targetX": 235,
"targetY": 37,
"t": 61729
},
{
"targetX": 237,
"targetY": 37,
"t": 61829
}
],
"isFromTouch": true
},
"terminationEvent": "cancel"
}
],
"config": {
"deviceStyle": "screen2",
"roamingStyle": "bounds1",
"receiverStyle": "full",
"safeZoneStyle": "safe-loose"
}
}

View file

@ -25,6 +25,12 @@
<p class="center">
The area below represents a simulated page with its corresponding screen, page, and gesture-reception area.
</p>
<p class="center">
NOTE: For setting up touch-based sequences from a normal computer, use of Firefox's touch-emulation mode is
advised. Chrome's version tends to produce touch events with subpixel coordinates, whereas Firefox's doesn't.
You may also need to manually tweak the final coordinate a little bit for cross-browser consistency.
</p>
<!-- Start the self-adjusting layout region -->
<div id="main-ux" class="flex">

View file

@ -20,31 +20,60 @@ namespace Testing {
}
}
private buildSyntheticTouchEvent(name: string, dict: TouchEventInit): TouchEvent {
let config = this.controller.recognizer.config;
let fullDict = {
srcElement: config.touchEventRoot,
view: window,
...dict
} as any;
return new Event('touchstart', fullDict) as TouchEvent;
}
replayTouchSample(sample: JSONObject<com.keyman.osk.InputSample>, state: string, identifier: number, otherTouches: Touch[]): Touch {
let config = this.controller.recognizer.config;
let event: TouchEvent;
const mappedSample = this.getSampleClientPos(sample);
let touch = new Touch({identifier: identifier,
target: config.targetRoot,
...mappedSample});
let touch: Touch;
let touchDict = {identifier: identifier,
target: config.targetRoot,
...mappedSample};
if(window['Touch'] !== undefined) {
touch = new Touch(touchDict);
} else {
// When not performing touch-emulation, some desktop browsers will leave `Touch` undefined.
touch = touchDict as any;
}
otherTouches = otherTouches || [];
let touchEventDict: TouchEventInit = {
bubbles: true,
touches: [touch],
changedTouches: [touch]
touches: [touch, ...otherTouches],
changedTouches: [touch],
}
let buildEvent = (type: string, dict: TouchEventInit) => {
if(window['TouchEvent'] !== undefined) {
return new TouchEvent(type, dict);
} else {
return this.buildSyntheticTouchEvent(type, dict);
}
}
switch(state) {
case 'start':
event = new TouchEvent('touchstart', touchEventDict);
event = buildEvent('touchstart', touchEventDict);
break;
case 'move':
event = new TouchEvent('touchmove', touchEventDict);
event = buildEvent('touchmove', touchEventDict);
break;
case 'end':
event = new TouchEvent('touchend', touchEventDict);
event = buildEvent('touchend', touchEventDict);
break;
}
@ -169,7 +198,9 @@ namespace Testing {
// We technically don't have access to 'samples' for the actual recorded object.
// Obtaining a 'deep copy' (though methodless) works around this nicely and
// matches the original `sequenceTestSpec`'s format to boot.
return JSON.parse(recorder.recordingsToJSON()) as RecordedCoordSequenceSet;
let recording = JSON.parse(recorder.recordingsToJSON()) as RecordedCoordSequenceSet;
recorder.clear();
return recording;
}
}
}

View file

@ -119,7 +119,7 @@ namespace com.keyman.osk {
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, this.config, this.safeBoundMaskMap[touch.identifier])) {
this.onInputMove(touch.identifier, sample);
} else {
this.onInputMoveCancel(touch.identifier);
this.onInputMoveCancel(touch.identifier, sample);
}
}
}
@ -134,9 +134,7 @@ namespace com.keyman.osk {
this.preventPropagation(event);
const sample = this.buildSampleFromTouch(touch);
this.onInputEnd(touch.identifier, sample);
this.onInputEnd(touch.identifier);
}
}
}