spiegel-keyman/common/predictive-text/unit_tests/headless/worker-model-compositor.js

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);
});
});
});