mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-07 09:25:33 +00:00
894 lines
34 KiB
JavaScript
894 lines
34 KiB
JavaScript
/*
|
|
* Integration tests for the model compositor with the trie model.
|
|
*/
|
|
|
|
const { models, wordBreakers } = require('../../build/intermediate');
|
|
|
|
var assert = require('chai').assert;
|
|
var TrieModel = require('../../build/intermediate').models.TrieModel;
|
|
var ModelCompositor = require('../../build/intermediate').ModelCompositor;
|
|
|
|
describe('ModelCompositor', function() {
|
|
describe('Prediction with 14.0+ models', function() {
|
|
describe('Basic suggestion generation', function() {
|
|
var plainModel = new TrieModel(jsonFixture('tries/english-1000'),
|
|
{wordBreaker: wordBreakers.default}
|
|
);
|
|
|
|
it('generates suggestions with expected properties', function() {
|
|
let compositor = new ModelCompositor(plainModel);
|
|
let context = {
|
|
left: 'th', startOfBuffer: true, endOfBuffer: true,
|
|
};
|
|
|
|
let inputTransform = {
|
|
insert: 'e',
|
|
deleteLeft: 0
|
|
};
|
|
|
|
let suggestions = compositor.predict(inputTransform, context);
|
|
suggestions.forEach(function(suggestion) {
|
|
// Suggstions are built based on the context state BEFORE the triggering
|
|
// input, replacing the prediction's root with the complete word.
|
|
//
|
|
// This is necessary, in part, for proper display-string construction.
|
|
assert.equal(suggestion.transform.deleteLeft, 2);
|
|
});
|
|
|
|
let keep = suggestions.find(function(suggestion) {
|
|
return suggestion.tag == 'keep';
|
|
});
|
|
|
|
assert.isDefined(keep);
|
|
assert.equal(keep.transform.insert, 'the ');
|
|
|
|
// Expect an appended space.
|
|
let expectedEntries = ['they ', 'there ', 'their ', 'these ', 'themselves '];
|
|
expectedEntries.forEach(function(entry) {
|
|
assert.isDefined(suggestions.find(function(suggestion) {
|
|
return suggestion.transform.insert == entry;
|
|
}));
|
|
});
|
|
});
|
|
|
|
it('properly handles suggestions after a backspace', function() {
|
|
let compositor = new ModelCompositor(plainModel);
|
|
let context = {
|
|
left: 'the ', startOfBuffer: true, endOfBuffer: true,
|
|
};
|
|
|
|
let inputTransform = {
|
|
insert: '',
|
|
deleteLeft: 1
|
|
};
|
|
|
|
let suggestions = compositor.predict(inputTransform, context);
|
|
suggestions.forEach(function(suggestion) {
|
|
// Suggestions always delete the full root of the suggestion.
|
|
//
|
|
// After a backspace, that means the text 'the' - 3 chars.
|
|
// Char 4 is for the original backspace, as suggstions are built
|
|
// based on the context state BEFORE the triggering input -
|
|
// here, a backspace.
|
|
assert.equal(suggestion.transform.deleteLeft, 4);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('applySuggestionCasing', function() {
|
|
let plainApplyCasing = function(caseToApply, text) {
|
|
switch(caseToApply) {
|
|
case 'lower':
|
|
return text.toLowerCase();
|
|
case 'upper':
|
|
return text.toUpperCase();
|
|
case 'initial':
|
|
return plainApplyCasing('upper', text.charAt(0)) . concat(text.substring(1));
|
|
default:
|
|
return text;
|
|
}
|
|
};
|
|
|
|
var plainCasedModel = new TrieModel(
|
|
jsonFixture('tries/english-1000'), {
|
|
languageUsesCasing: true,
|
|
applyCasing: plainApplyCasing,
|
|
wordBreaker: wordBreakers.default,
|
|
searchTermToKey: function(text) {
|
|
// We're dealing with very simple English text; no need to normalize or remove diacritics here.
|
|
return applyCasing('lower', text);
|
|
}
|
|
}
|
|
);
|
|
|
|
it('properly cases suggestions with no suggestion root', function() {
|
|
var compositor = new ModelCompositor(plainCasedModel);
|
|
|
|
let suggestion = {
|
|
transform: {
|
|
insert: 'the',
|
|
deleteLeft: 0
|
|
},
|
|
displayAs: 'the'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, '', 'initial');
|
|
assert.equal(suggestion.displayAs, 'The');
|
|
assert.equal(suggestion.transform.insert, 'The');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'thE',
|
|
deleteLeft: 0
|
|
},
|
|
displayAs: 'thE'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, '', 'initial');
|
|
assert.equal(suggestion.displayAs, 'ThE');
|
|
assert.equal(suggestion.transform.insert, 'ThE');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'the',
|
|
deleteLeft: 0
|
|
},
|
|
displayAs: 'the'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, '', 'upper');
|
|
assert.equal(suggestion.displayAs, 'THE');
|
|
assert.equal(suggestion.transform.insert, 'THE');
|
|
});
|
|
|
|
it('properly cases suggestions that fully replace the suggestion root', function() {
|
|
var compositor = new ModelCompositor(plainCasedModel);
|
|
|
|
let suggestion = {
|
|
transform: {
|
|
insert: 'therefore',
|
|
deleteLeft: 3
|
|
},
|
|
displayAs: 'therefore'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'initial');
|
|
assert.equal(suggestion.displayAs, 'Therefore');
|
|
assert.equal(suggestion.transform.insert, 'Therefore');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'thereFore',
|
|
deleteLeft: 3
|
|
},
|
|
displayAs: 'thereFore'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'initial');
|
|
assert.equal(suggestion.displayAs, 'ThereFore');
|
|
assert.equal(suggestion.transform.insert, 'ThereFore');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'therefore',
|
|
deleteLeft: 3
|
|
},
|
|
displayAs: 'therefore'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'upper');
|
|
assert.equal(suggestion.displayAs, 'THEREFORE');
|
|
assert.equal(suggestion.transform.insert, 'THEREFORE');
|
|
});
|
|
|
|
it('properly cases suggestions that do not fully replace the suggestion root', function() {
|
|
var compositor = new ModelCompositor(plainCasedModel);
|
|
|
|
let suggestion = {
|
|
transform: {
|
|
insert: 'erefore',
|
|
deleteLeft: 1
|
|
},
|
|
displayAs: 'therefore'
|
|
};
|
|
|
|
// When integrated, the 'the' string comes from a wordbreak operation on the current context.
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'initial');
|
|
assert.equal(suggestion.displayAs, 'Therefore');
|
|
assert.equal(suggestion.transform.insert, 'Therefore');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'ereFore',
|
|
deleteLeft: 1
|
|
},
|
|
displayAs: 'thereFore'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'initial');
|
|
assert.equal(suggestion.displayAs, 'ThereFore');
|
|
assert.equal(suggestion.transform.insert, 'ThereFore');
|
|
|
|
suggestion = {
|
|
transform: {
|
|
insert: 'erefore',
|
|
deleteLeft: 1
|
|
},
|
|
displayAs: 'therefore'
|
|
};
|
|
|
|
compositor.applySuggestionCasing(suggestion, 'the', 'upper');
|
|
assert.equal(suggestion.displayAs, 'THEREFORE');
|
|
assert.equal(suggestion.transform.insert, 'THEREFORE');
|
|
});
|
|
});
|
|
|
|
describe('Model uses default-style keying, no casing', function () {
|
|
var uncasedModel = new TrieModel(
|
|
jsonFixture('tries/english-1000'), {
|
|
languageUsesCasing: false,
|
|
wordBreaker: wordBreakers.default,
|
|
searchTermToKey: function(text) {
|
|
// We're dealing with very simple English text; without casing, there's no keying to be done
|
|
// for such simple data. No need to normalize or remove diacritics here.
|
|
return text;
|
|
}
|
|
}
|
|
);
|
|
|
|
it('should produce suggestions from uncased input', function() {
|
|
let model = uncasedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'th', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
|
|
it('should not produce suggestions from cased input', function() {
|
|
let model = uncasedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'TH', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'R', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'E', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// We should only receive a 'keep' suggestion.
|
|
assert.equal(suggestions.length, 1);
|
|
assert.equal(suggestions[0].tag, 'keep');
|
|
});
|
|
});
|
|
|
|
describe('Model uses default-style keying, provides casing', function () {
|
|
let applyCasing = function(caseToApply, text) {
|
|
switch(caseToApply) {
|
|
case 'lower':
|
|
return text.toLowerCase();
|
|
case 'upper':
|
|
return text.toUpperCase();
|
|
case 'initial':
|
|
return applyCasing('upper', text.charAt(0)) . concat(text.substring(1));
|
|
default:
|
|
return text;
|
|
}
|
|
};
|
|
|
|
var casedModel = new TrieModel(
|
|
jsonFixture('tries/english-1000'), {
|
|
languageUsesCasing: true,
|
|
applyCasing: applyCasing,
|
|
wordBreaker: wordBreakers.default,
|
|
searchTermToKey: function(text) {
|
|
// We're dealing with very simple English text; no need to normalize or remove diacritics here.
|
|
return applyCasing('lower', text);
|
|
}
|
|
}
|
|
);
|
|
|
|
it('should produce suggestions from uncased input', function() {
|
|
let model = casedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'th', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
|
|
it('should produce capitalized suggestions from fully-uppercased input', function() {
|
|
let model = casedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'TH', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'R', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'E', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'THE' || s.displayAs === '“THE”'; })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('THR'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
|
|
it('should produce "initial-case" suggestions from input with an initial capital', function() {
|
|
let model = casedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'Th', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'The' || s.displayAs === '“The”'; })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('Thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
|
|
it('also from input with partial capitalization when including an initial capital', function() {
|
|
let model = casedModel;
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'TH', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('The'); })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('Thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Prediction with legacy Models (12.0 / 13.0)', function() {
|
|
it('should compose suggestions from a fat-fingered keypress (no keying needed)', function () {
|
|
var model = new TrieModel(
|
|
jsonFixture('tries/english-1000')
|
|
);
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'th', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
|
|
it('should compose suggestions from a fat-fingered keypress (keying needed)', function () {
|
|
var model = new TrieModel(
|
|
jsonFixture('tries/english-1000')
|
|
);
|
|
var composite = new ModelCompositor(model);
|
|
|
|
// Initialize context
|
|
let context = {
|
|
left: 'Th', startOfBuffer: false, endOfBuffer: true,
|
|
};
|
|
composite.predict({insert: '', deleteLeft: 0}, context);
|
|
|
|
// Pretend to fat finger "the" as "thr"
|
|
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
|
|
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
|
|
var suggestions = composite.predict([thr, the], context);
|
|
|
|
// Get the top suggest for 'the' and 'thr*'.
|
|
// As of 15.0+, because of #5429, the keep suggestion `"The"` will not be merged
|
|
// with the model's suggestion of `the`.
|
|
var capTheSuggestion = suggestions.filter(function (s) { return s.displayAs === '“The”'; })[0];
|
|
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the'})[0];
|
|
var thrSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('thr'); })[0];
|
|
|
|
// Sanity check: do we have actual real-valued probabilities?
|
|
assert.isAbove(thrSuggestion.p, 0.0);
|
|
assert.isBelow(thrSuggestion.p, 1.0);
|
|
assert.isAbove(theSuggestion.p, 0.0);
|
|
assert.isBelow(theSuggestion.p, 1.0);
|
|
assert.equal(capTheSuggestion.tag, 'keep'); // The keep suggestion
|
|
// 'the' should be the intended the result here.
|
|
assert.isAbove(theSuggestion.p, thrSuggestion.p);
|
|
});
|
|
});
|
|
|
|
// The nomenclature's a minor sneak-peek from child PRs.
|
|
describe('toAnnotatedSuggestion', function() {
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'hello',
|
|
deleteLeft: 0,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
displayAs: 'hello'
|
|
};
|
|
|
|
let englishPunctuation = {
|
|
quotesForKeepSuggestion: { open: `“`, close: `”`},
|
|
insertAfterWord: ' '
|
|
};
|
|
|
|
let angledPunctuation = {
|
|
quotesForKeepSuggestion: { open: `«`, close: `»`},
|
|
insertAfterWord: " "
|
|
}
|
|
|
|
describe("'keep'", function() {
|
|
let annotationTest = function(punctuation, displayText, quoteStyle) {
|
|
let options = {
|
|
punctuation: punctuation
|
|
};
|
|
|
|
let model = new models.DummyModel(options);
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
var keep;
|
|
if(quoteStyle) {
|
|
keep = compositor.toAnnotatedSuggestion(baseSuggestion, 'keep', quoteStyle);
|
|
} else {
|
|
keep = compositor.toAnnotatedSuggestion(baseSuggestion, 'keep');
|
|
}
|
|
|
|
// Make sure we didn't accidentally leak any mutations to the parameter.
|
|
assert.notDeepEqual(keep, baseSuggestion);
|
|
|
|
assert.equal(keep.displayAs, displayText);
|
|
assert.equal(keep.tag, 'keep');
|
|
}
|
|
|
|
it('quoteBehavior: (.default)', function() {
|
|
annotationTest(englishPunctuation, "“hello”");
|
|
annotationTest(angledPunctuation, "«hello»");
|
|
});
|
|
|
|
it('quoteBehavior: .useQuotes', function() {
|
|
annotationTest(englishPunctuation, "“hello”", models.QuoteBehavior.useQuotes);
|
|
annotationTest(angledPunctuation, "«hello»", models.QuoteBehavior.useQuotes);
|
|
});
|
|
|
|
it('quoteBehavior: .noQuotes', function() {
|
|
annotationTest(englishPunctuation, "hello", models.QuoteBehavior.noQuotes);
|
|
annotationTest(angledPunctuation, "hello", models.QuoteBehavior.noQuotes);
|
|
});
|
|
|
|
it.skip('RTL test', function() {
|
|
// TODO:
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('acceptSuggestion', function() {
|
|
let acceptanceTest = function(punctuation, suggestion, context, postTransform) {
|
|
let options = {
|
|
punctuation: punctuation
|
|
};
|
|
|
|
let model = new models.DummyModel(options);
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
return compositor.acceptSuggestion(suggestion, context, postTransform);
|
|
}
|
|
|
|
let englishPunctuation = {
|
|
quotesForKeepSuggestion: { open: `“`, close: `”`},
|
|
insertAfterWord: ' '
|
|
};
|
|
|
|
let angledPunctuation = {
|
|
quotesForKeepSuggestion: { open: `«`, close: `»`},
|
|
insertAfterWord: " "
|
|
}
|
|
|
|
it('first word of context, postTransform provided, .deleteLeft = 0', function() {
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'hello ',
|
|
deleteLeft: 2,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
id: 1,
|
|
displayAs: 'hello'
|
|
};
|
|
|
|
let baseContext = {
|
|
left: 'he', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'l',
|
|
deleteLeft: 0
|
|
}
|
|
|
|
let reversion = acceptanceTest(englishPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
// Check #1: Does the returned reversion properly revert the context to its pre-application state?
|
|
// Does this include characters not considered when the Suggestion was built?
|
|
let unappliedContext = models.applyTransform(postTransform, baseContext);
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
assert.equal(appliedContext.left, "hello ");
|
|
|
|
let revertedContext = models.applyTransform(reversion.transform, appliedContext);
|
|
assert.deepEqual(revertedContext, unappliedContext);
|
|
|
|
// Check #2: Are the correct display strings built, depending on the active model's punctuation?
|
|
assert.equal(reversion.displayAs, "“hel”"); // text should _basically_ be a quoted version of `preApplyContext.left`
|
|
|
|
let angledReversion = acceptanceTest(angledPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(angledReversion.displayAs, "«hel»");
|
|
});
|
|
|
|
it('second word of context, postTransform provided, .deleteLeft = 0', function() {
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'world ',
|
|
deleteLeft: 3,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
id: 0,
|
|
displayAs: 'world'
|
|
};
|
|
|
|
let baseContext = {
|
|
left: 'hello wot', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'l',
|
|
deleteLeft: 0
|
|
}
|
|
|
|
let reversion = acceptanceTest(englishPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
// Check #1: Does the returned reversion properly revert the context to its pre-application state?
|
|
// Does this include characters not considered when the Suggestion was built?
|
|
let unappliedContext = models.applyTransform(postTransform, baseContext);
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
assert.equal(appliedContext.left, "hello world ");
|
|
|
|
let revertedContext = models.applyTransform(reversion.transform, appliedContext);
|
|
assert.deepEqual(revertedContext, unappliedContext);
|
|
|
|
// Check #2: Are the correct display strings built, depending on the active model's punctuation?
|
|
assert.equal(reversion.displayAs, "“wotl”"); // text should _basically_ be a quoted version of `preApplyContext.left`
|
|
|
|
let angledReversion = acceptanceTest(angledPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(angledReversion.displayAs, "«wotl»");
|
|
});
|
|
|
|
it('second word of context, postTransform undefined', function() {
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'world ',
|
|
deleteLeft: 3,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
id: 0,
|
|
displayAs: 'world'
|
|
};
|
|
|
|
let baseContext = {
|
|
left: 'hello wot', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
let reversion = acceptanceTest(englishPunctuation, baseSuggestion, baseContext);
|
|
assert.equal(reversion.transformId, baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
// Check #1: Does the returned reversion properly revert the context to its pre-application state?
|
|
// Does this include characters not considered when the Suggestion was built?
|
|
let unappliedContext = models.applyTransform({insert: '', deleteLeft: 0}, baseContext); // to clone the original context.
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
assert.equal(appliedContext.left, "hello world ");
|
|
|
|
let revertedContext = models.applyTransform(reversion.transform, appliedContext);
|
|
assert.deepEqual(revertedContext, unappliedContext);
|
|
|
|
// Check #2: Are the correct display strings built, depending on the active model's punctuation?
|
|
assert.equal(reversion.displayAs, "“wot”"); // text should _basically_ be a quoted version of `preApplyContext.left`
|
|
|
|
let angledReversion = acceptanceTest(angledPunctuation, baseSuggestion, baseContext);
|
|
assert.equal(angledReversion.displayAs, "«wot»");
|
|
});
|
|
|
|
it('first word of context + postTransform provided, .deleteLeft > 0', function() {
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'hello ',
|
|
deleteLeft: 2,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
id: 0,
|
|
displayAs: 'hello'
|
|
};
|
|
|
|
let baseContext = {
|
|
left: 'he', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'i',
|
|
deleteLeft: 1
|
|
}
|
|
|
|
let reversion = acceptanceTest(englishPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
// Check #1: Does the returned reversion properly revert the context to its pre-application state?
|
|
// Does this include characters not considered when the Suggestion was built?
|
|
let unappliedContext = models.applyTransform(postTransform, baseContext);
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
assert.equal(appliedContext.left, "hello ");
|
|
|
|
let revertedContext = models.applyTransform(reversion.transform, appliedContext);
|
|
assert.deepEqual(revertedContext, unappliedContext);
|
|
|
|
// Check #2: Are the correct display strings built, depending on the active model's punctuation?
|
|
assert.equal(reversion.displayAs, "“hi”"); // text should _basically_ be a quoted version of `preApplyContext.left`
|
|
|
|
let angledReversion = acceptanceTest(angledPunctuation, baseSuggestion, baseContext, postTransform);
|
|
assert.equal(angledReversion.displayAs, "«hi»");
|
|
});
|
|
});
|
|
|
|
describe('acceptReversion', function() {
|
|
let executeAcceptance = function(model, suggestion, context, postTransform) {
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
return {compositor: compositor, reversion: compositor.acceptSuggestion(suggestion, context, postTransform)};
|
|
}
|
|
|
|
let englishPunctuation = {
|
|
quotesForKeepSuggestion: { open: `“`, close: `”`},
|
|
insertAfterWord: ' '
|
|
};
|
|
|
|
// While this isn't a state the LMLayer should ever operate within, this provides
|
|
// a useful base state for developing further tests against the method.
|
|
it('model without traversals: returns appropriate suggestions upon reversion', function() {
|
|
// This setup matches 'acceptSuggestion' the test case
|
|
// it('first word of context + postTransform provided, .deleteLeft > 0')
|
|
// seen earlier in the file.
|
|
|
|
let baseSuggestion = {
|
|
transform: {
|
|
insert: 'hello ',
|
|
deleteLeft: 2,
|
|
id: 0
|
|
},
|
|
transformId: 0,
|
|
id: 0,
|
|
displayAs: 'hello'
|
|
};
|
|
|
|
let baseContext = {
|
|
left: 'he', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'i',
|
|
deleteLeft: 1
|
|
}
|
|
|
|
let model = new models.DummyModel({punctuation: englishPunctuation});
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
assert.equal(appliedContext.left, "hello ");
|
|
|
|
let suggestions = compositor.applyReversion(reversion, appliedContext);
|
|
|
|
// As this test is a bit... 'hard-wired', we only get the 'keep' suggestion.
|
|
// It should still be accurate, though.
|
|
assert.equal(suggestions.length, 1);
|
|
|
|
let expectedTransform = {
|
|
insert: ' ', // Keeps current context the same, though it adds a wordbreak.
|
|
deleteLeft: 0
|
|
}
|
|
assert.deepEqual(suggestions[0].transform, expectedTransform);
|
|
});
|
|
|
|
it('model with traversals: returns appropriate suggestions upon reversion', function() {
|
|
// This setup matches 'acceptSuggestion' the test case
|
|
// it('first word of context + postTransform provided, .deleteLeft > 0')
|
|
// seen earlier in the file.
|
|
|
|
let baseContext = {
|
|
left: 'he', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'i',
|
|
deleteLeft: 1,
|
|
id: 13
|
|
}
|
|
|
|
let model = new models.TrieModel(jsonFixture('tries/english-1000'), {punctuation: englishPunctuation});
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
let initialSuggestions = compositor.predict(postTransform, baseContext);
|
|
let keepSuggestion = initialSuggestions[0];
|
|
assert.equal(keepSuggestion.tag, 'keep'); // corresponds to `postTransform`, but the transform isn't equal.
|
|
|
|
let baseSuggestion = initialSuggestions[1];
|
|
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, -baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
let reversionSuggestions = compositor.applyReversion(reversion, appliedContext);
|
|
|
|
// The returned suggestion list should match the original suggestion list.
|
|
assert.deepEqual(reversionSuggestions, initialSuggestions);
|
|
});
|
|
|
|
it('model with traversals: properly tracks context state', function() {
|
|
// Could be merged with the previous test case, but I think it's good to have the error
|
|
// sets flagged separately.
|
|
|
|
let baseContext = {
|
|
left: 'he', startOfBuffer: true, endOfBuffer: true
|
|
}
|
|
|
|
// Represents the keystroke that triggered the suggestion. It's not technically part
|
|
// of the Context when the suggestion is built.
|
|
let postTransform = {
|
|
insert: 'i',
|
|
deleteLeft: 1,
|
|
id: 13
|
|
}
|
|
|
|
let model = new models.TrieModel(jsonFixture('tries/english-1000'), {punctuation: englishPunctuation});
|
|
let compositor = new ModelCompositor(model);
|
|
|
|
let initialSuggestions = compositor.predict(postTransform, baseContext);
|
|
let keepSuggestion = initialSuggestions[0];
|
|
assert.equal(keepSuggestion.tag, 'keep'); // corresponds to `postTransform`, but the transform isn't equal.
|
|
|
|
let baseContextState = compositor.contextTracker.analyzeState(model, baseContext);
|
|
assert.equal(compositor.contextTracker.count, 1);
|
|
|
|
let baseSuggestion = initialSuggestions[1];
|
|
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
|
|
assert.equal(reversion.transformId, -baseSuggestion.transformId);
|
|
assert.equal(reversion.id, -baseSuggestion.id);
|
|
|
|
// Accepting the suggestion adds an extra context state.
|
|
assert.equal(compositor.contextTracker.count, 2);
|
|
|
|
// The replacement should be marked on the context-tracking token.
|
|
assert.isOk(baseContextState.tail.replacement);
|
|
|
|
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
|
|
compositor.applyReversion(reversion, appliedContext);
|
|
|
|
// Reverting the suggestion should remove that extra state.
|
|
assert.equal(compositor.contextTracker.count, 1);
|
|
assert.equal(compositor.contextTracker.item(0), baseContextState);
|
|
|
|
// The replacement should no longer be marked for the context-tracking token.
|
|
assert.isNotOk(baseContextState.tail.replacement);
|
|
});
|
|
});
|
|
});
|