mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-07 22:47:29 +00:00
fix(web): identified sequence-repro issues
This commit is contained in:
parent
5cbe959bc0
commit
bacfe6d507
9 changed files with 456 additions and 65 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue