yomichan/test/test-options-util.js

1225 lines
46 KiB
JavaScript
Raw Normal View History

2020-09-20 15:33:12 +00:00
/*
* Copyright (C) 2020-2022 Yomichan Authors
2020-09-20 15:33:12 +00:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const fs = require('fs');
const url = require('url');
const path = require('path');
const assert = require('assert');
const {testMain} = require('../dev/util');
2020-09-20 15:33:12 +00:00
const {VM} = require('../dev/vm');
function createVM(extDir) {
const chrome = {
runtime: {
getURL(path2) {
return url.pathToFileURL(path.join(extDir, path2.replace(/^\//, ''))).href;
}
}
};
async function fetch(url2) {
const filePath = url.fileURLToPath(url2);
await Promise.resolve();
const content = fs.readFileSync(filePath, {encoding: null});
return {
ok: true,
status: 200,
statusText: 'OK',
text: async () => Promise.resolve(content.toString('utf8')),
json: async () => Promise.resolve(JSON.parse(content.toString('utf8')))
};
}
const vm = new VM({chrome, fetch});
vm.execute([
Move mixed/js (#1383) * Move mixed/js/core.js to js/core.js * Move mixed/js/yomichan.js to js/yomichan.js * Move mixed/js/timer.js to js/debug/timer.js * Move mixed/js/hotkey-handler.js to js/input/hotkey-handler.js * Move mixed/js/hotkey-help-controller.js to js/input/hotkey-help-controller.js * Move mixed/js/hotkey-util.js to js/input/hotkey-util.js * Move mixed/js/audio-system.js to js/input/audio-system.js * Move mixed/js/media-loader.js to js/input/media-loader.js * Move mixed/js/text-to-speech-audio.js to js/input/text-to-speech-audio.js * Move mixed/js/comm.js to js/comm/cross-frame-api.js * Move mixed/js/api.js to js/comm/api.js * Move mixed/js/frame-client.js to js/comm/frame-client.js * Move mixed/js/frame-endpoint.js to js/comm/frame-endpoint.js * Move mixed/js/display.js to js/display/display.js * Move mixed/js/display-audio.js to js/display/display-audio.js * Move mixed/js/display-generator.js to js/display/display-generator.js * Move mixed/js/display-history.js to js/display/display-history.js * Move mixed/js/display-notification.js to js/display/display-notification.js * Move mixed/js/display-profile-selection.js to js/display/display-profile-selection.js * Move mixed/js/japanese.js to js/language/japanese-util.js * Move mixed/js/dictionary-data-util.js to js/language/dictionary-data-util.js * Move mixed/js/document-focus-controller.js to js/dom/document-focus-controller.js * Move mixed/js/document-util.js to js/dom/document-util.js * Move mixed/js/dom-data-binder.js to js/dom/dom-data-binder.js * Move mixed/js/html-template-collection.js to js/dom/html-template-collection.js * Move mixed/js/panel-element.js to js/dom/panel-element.js * Move mixed/js/popup-menu.js to js/dom/popup-menu.js * Move mixed/js/selector-observer.js to js/dom/selector-observer.js * Move mixed/js/scroll.js to js/dom/window-scroll.js * Move mixed/js/text-scanner.js to js/language/text-scanner.js * Move mixed/js/cache-map.js to js/general/cache-map.js * Move mixed/js/object-property-accessor.js to js/general/object-property-accessor.js * Move mixed/js/task-accumulator.js to js/general/task-accumulator.js * Move mixed/js/environment.js to js/background/environment.js * Move mixed/js/dynamic-loader.js to js/scripting/dynamic-loader.js * Move mixed/js/dynamic-loader-sentinel.js to js/scripting/dynamic-loader-sentinel.js
2021-02-14 03:52:28 +00:00
'js/core.js',
'js/general/cache-map.js',
Move bg/js (#1387) * Move bg/js/anki.js to js/comm/anki.js * Move bg/js/mecab.js to js/comm/mecab.js * Move bg/js/search-main.js to js/display/search-main.js * Move bg/js/template-patcher.js to js/templates/template-patcher.js * Move bg/js/template-renderer-frame-api.js to js/templates/template-renderer-frame-api.js * Move bg/js/template-renderer-frame-main.js to js/templates/template-renderer-frame-main.js * Move bg/js/template-renderer-proxy.js to js/templates/template-renderer-proxy.js * Move bg/js/template-renderer.js to js/templates/template-renderer.js * Move bg/js/media-utility.js to js/media/media-utility.js * Move bg/js/native-simple-dom-parser.js to js/dom/native-simple-dom-parser.js * Move bg/js/simple-dom-parser.js to js/dom/simple-dom-parser.js * Move bg/js/audio-downloader.js to js/media/audio-downloader.js * Move bg/js/deinflector.js to js/language/deinflector.js * Move bg/js/backend.js to js/background/backend.js * Move bg/js/translator.js to js/language/translator.js * Move bg/js/search-display-controller.js to js/display/search-display-controller.js * Move bg/js/request-builder.js to js/background/request-builder.js * Move bg/js/text-source-map.js to js/general/text-source-map.js * Move bg/js/clipboard-reader.js to js/comm/clipboard-reader.js * Move bg/js/clipboard-monitor.js to js/comm/clipboard-monitor.js * Move bg/js/query-parser.js to js/display/query-parser.js * Move bg/js/profile-conditions.js to js/background/profile-conditions.js * Move bg/js/dictionary-database.js to js/language/dictionary-database.js * Move bg/js/dictionary-importer.js to js/language/dictionary-importer.js * Move bg/js/anki-note-builder.js to js/data/anki-note-builder.js * Move bg/js/anki-note-data.js to js/data/anki-note-data.js * Move bg/js/database.js to js/data/database.js * Move bg/js/json-schema.js to js/data/json-schema.js * Move bg/js/options.js to js/data/options-util.js * Move bg/js/background-main.js to js/background/background-main.js * Move bg/js/permissions-util.js to js/data/permissions-util.js * Move bg/js/context-main.js to js/pages/action-popup-main.js * Move bg/js/generic-page-main.js to js/pages/generic-page-main.js * Move bg/js/info-main.js to js/pages/info-main.js * Move bg/js/permissions-main.js to js/pages/permissions-main.js * Move bg/js/welcome-main.js to js/pages/welcome-main.js
2021-02-14 16:19:54 +00:00
'js/data/json-schema.js',
'js/templates/template-patcher.js',
'js/data/options-util.js'
2020-09-20 15:33:12 +00:00
]);
return vm;
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function createProfileOptionsTestData1() {
return {
version: 14,
2020-09-20 15:33:12 +00:00
general: {
enable: true,
enableClipboardPopups: false,
resultOutputMode: 'group',
debugInfo: false,
maxResults: 32,
showAdvanced: false,
popupDisplayMode: 'default',
popupWidth: 400,
popupHeight: 250,
popupHorizontalOffset: 0,
popupVerticalOffset: 10,
popupHorizontalOffset2: 10,
popupVerticalOffset2: 0,
popupHorizontalTextPosition: 'below',
popupVerticalTextPosition: 'before',
popupScalingFactor: 1,
popupScaleRelativeToPageZoom: false,
popupScaleRelativeToVisualViewport: true,
showGuide: true,
compactTags: false,
compactGlossaries: false,
mainDictionary: '',
popupTheme: 'default',
popupOuterTheme: 'default',
customPopupCss: '',
customPopupOuterCss: '',
enableWanakana: true,
enableClipboardMonitor: false,
showPitchAccentDownstepNotation: true,
showPitchAccentPositionNotation: true,
showPitchAccentGraph: false,
showIframePopupsInRootFrame: false,
useSecurePopupFrameUrl: true,
usePopupShadowDom: true
},
audio: {
enabled: true,
sources: ['jpod101', 'text-to-speech', 'custom'],
2020-09-20 15:33:12 +00:00
volume: 100,
autoPlay: false,
customSourceUrl: 'http://localhost/audio.mp3?term={expression}&reading={reading}',
textToSpeechVoice: 'example-voice'
2020-09-20 15:33:12 +00:00
},
scanning: {
middleMouse: true,
touchInputEnabled: true,
selectText: true,
alphanumeric: true,
autoHideResults: false,
delay: 20,
length: 10,
modifier: 'shift',
deepDomScan: false,
popupNestingMaxDepth: 0,
enablePopupSearch: false,
enableOnPopupExpressions: false,
enableOnSearchPage: true,
enableSearchTags: false,
layoutAwareScan: false
},
translation: {
convertHalfWidthCharacters: 'false',
convertNumericCharacters: 'false',
convertAlphabeticCharacters: 'false',
convertHiraganaToKatakana: 'false',
convertKatakanaToHiragana: 'variant',
collapseEmphaticSequences: 'false'
},
dictionaries: {
'Test Dictionary': {
priority: 0,
enabled: true,
allowSecondarySearches: false
}
},
2020-09-20 15:33:12 +00:00
parsing: {
enableScanningParser: true,
enableMecabParser: false,
selectedParser: null,
termSpacing: true,
readingMode: 'hiragana'
},
anki: {
enable: false,
server: 'http://127.0.0.1:8765',
tags: ['yomichan'],
sentenceExt: 200,
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}},
duplicateScope: 'collection',
fieldTemplates: null
}
};
}
function createOptionsTestData1() {
return {
profiles: [
{
name: 'Default',
options: createProfileOptionsTestData1(),
conditionGroups: [
{
conditions: [
{
type: 'popupLevel',
operator: 'equal',
value: 1
},
{
type: 'popupLevel',
operator: 'notEqual',
value: 0
},
{
type: 'popupLevel',
operator: 'lessThan',
value: 3
},
{
type: 'popupLevel',
operator: 'greaterThan',
value: 0
},
{
type: 'popupLevel',
operator: 'lessThanOrEqual',
value: 2
},
{
type: 'popupLevel',
operator: 'greaterThanOrEqual',
value: 1
}
]
},
{
conditions: [
{
type: 'url',
operator: 'matchDomain',
value: 'example.com'
},
{
type: 'url',
operator: 'matchRegExp',
value: 'example\\.com'
}
]
},
{
conditions: [
{
type: 'modifierKeys',
operator: 'are',
value: [
'ctrl',
'shift'
]
},
{
type: 'modifierKeys',
operator: 'areNot',
value: [
'alt',
'shift'
]
},
{
type: 'modifierKeys',
operator: 'include',
value: 'alt'
},
{
type: 'modifierKeys',
operator: 'notInclude',
value: 'ctrl'
}
]
}
]
}
],
profileCurrent: 0,
version: 2,
global: {
database: {
prefixWildcardsSupported: false
}
}
};
}
function createProfileOptionsUpdatedTestData1() {
return {
general: {
enable: true,
resultOutputMode: 'group',
debugInfo: false,
maxResults: 32,
showAdvanced: false,
popupDisplayMode: 'default',
popupWidth: 400,
popupHeight: 250,
popupHorizontalOffset: 0,
popupVerticalOffset: 10,
popupHorizontalOffset2: 10,
popupVerticalOffset2: 0,
popupHorizontalTextPosition: 'below',
popupVerticalTextPosition: 'before',
popupScalingFactor: 1,
popupScaleRelativeToPageZoom: false,
popupScaleRelativeToVisualViewport: true,
showGuide: true,
compactTags: false,
glossaryLayoutMode: 'default',
2020-09-20 15:33:12 +00:00
mainDictionary: '',
popupTheme: 'light',
popupOuterTheme: 'light',
2020-09-20 15:33:12 +00:00
customPopupCss: '',
customPopupOuterCss: '',
enableWanakana: true,
showPitchAccentDownstepNotation: true,
showPitchAccentPositionNotation: true,
showPitchAccentGraph: false,
showIframePopupsInRootFrame: false,
useSecurePopupFrameUrl: true,
usePopupShadowDom: true,
usePopupWindow: false,
popupCurrentIndicatorMode: 'triangle',
popupActionBarVisibility: 'auto',
popupActionBarLocation: 'top',
frequencyDisplayMode: 'split-tags-grouped',
termDisplayMode: 'ruby',
sortFrequencyDictionary: null,
sortFrequencyDictionaryOrder: 'descending'
2020-09-20 15:33:12 +00:00
},
audio: {
enabled: true,
sources: [
{
type: 'jpod101',
url: '',
voice: ''
},
{
type: 'text-to-speech',
url: '',
voice: 'example-voice'
},
{
type: 'custom',
url: 'http://localhost/audio.mp3?term={term}&reading={reading}',
voice: ''
}
],
2020-09-20 15:33:12 +00:00
volume: 100,
autoPlay: false
2020-09-20 15:33:12 +00:00
},
scanning: {
touchInputEnabled: true,
selectText: true,
alphanumeric: true,
autoHideResults: false,
delay: 20,
length: 10,
deepDomScan: false,
popupNestingMaxDepth: 0,
enablePopupSearch: false,
enableOnPopupExpressions: false,
enableOnSearchPage: true,
enableSearchTags: false,
layoutAwareScan: false,
hideDelay: 0,
pointerEventsEnabled: false,
matchTypePrefix: false,
hidePopupOnCursorExit: false,
hidePopupOnCursorExitDelay: 0,
preventMiddleMouse: {
onWebPages: false,
onPopupPages: false,
onSearchPages: false,
onSearchQuery: false
},
2020-09-20 15:33:12 +00:00
inputs: [
{
include: 'shift',
exclude: 'mouse0',
2020-09-20 15:33:12 +00:00
types: {
mouse: true,
touch: false,
pen: false
},
options: {
showAdvanced: false,
searchTerms: true,
searchKanji: true,
scanOnTouchMove: true,
2020-09-20 15:33:12 +00:00
scanOnPenHover: true,
scanOnPenPress: true,
scanOnPenRelease: false,
preventTouchScrolling: true
2020-09-20 15:33:12 +00:00
}
},
{
include: 'mouse2',
exclude: '',
types: {
mouse: true,
touch: false,
pen: false
},
options: {
showAdvanced: false,
searchTerms: true,
searchKanji: true,
scanOnTouchMove: true,
2020-09-20 15:33:12 +00:00
scanOnPenHover: true,
scanOnPenPress: true,
scanOnPenRelease: false,
preventTouchScrolling: true
2020-09-20 15:33:12 +00:00
}
},
{
include: '',
exclude: '',
types: {
mouse: false,
touch: true,
pen: true
},
options: {
showAdvanced: false,
searchTerms: true,
searchKanji: true,
scanOnTouchMove: true,
2020-09-20 15:33:12 +00:00
scanOnPenHover: true,
scanOnPenPress: true,
scanOnPenRelease: false,
preventTouchScrolling: true
2020-09-20 15:33:12 +00:00
}
}
]
},
translation: {
convertHalfWidthCharacters: 'false',
convertNumericCharacters: 'false',
convertAlphabeticCharacters: 'false',
convertHiraganaToKatakana: 'false',
convertKatakanaToHiragana: 'variant',
collapseEmphaticSequences: 'false',
textReplacements: {
searchOriginal: true,
groups: []
}
2020-09-20 15:33:12 +00:00
},
dictionaries: [
{
name: 'Test Dictionary',
priority: 0,
enabled: true,
allowSecondarySearches: false,
definitionsCollapsible: 'not-collapsible'
}
],
2020-09-20 15:33:12 +00:00
parsing: {
enableScanningParser: true,
enableMecabParser: false,
selectedParser: null,
termSpacing: true,
readingMode: 'hiragana'
},
anki: {
enable: false,
server: 'http://127.0.0.1:8765',
tags: ['yomichan'],
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}},
duplicateScope: 'collection',
duplicateScopeCheckAllModels: false,
displayTags: 'never',
checkForDuplicates: true,
fieldTemplates: null,
suspendNewCards: false
},
sentenceParsing: {
scanExtent: 200,
terminationCharacterMode: 'custom',
terminationCharacters: [
{enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false},
{enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false},
{enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false},
{enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false},
{enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '︒', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '︕', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '︖', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
{enabled: true, character1: '︙', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}
]
},
inputs: {
hotkeys: [
{action: 'close', argument: '', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true},
{action: 'focusSearchBox', argument: '', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true},
{action: 'previousEntry', argument: '3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'nextEntry', argument: '3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'lastEntry', argument: '', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'firstEntry', argument: '', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'previousEntry', argument: '1', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'nextEntry', argument: '1', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'historyBackward', argument: '', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'historyForward', argument: '', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'addNoteKanji', argument: '', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'addNoteTermKanji', argument: '', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'addNoteTermKana', argument: '', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'playAudio', argument: '', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'viewNote', argument: '', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
{action: 'copyHostSelection', argument: '', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true}
]
},
popupWindow: {
width: 400,
height: 250,
left: 0,
top: 0,
useLeft: false,
useTop: false,
windowType: 'popup',
windowState: 'normal'
},
clipboard: {
enableBackgroundMonitor: false,
enableSearchPageMonitor: false,
autoSearchContent: true,
maximumSearchLength: 1000
},
accessibility: {
forceGoogleDocsHtmlRendering: false
2020-09-20 15:33:12 +00:00
}
};
}
function createOptionsUpdatedTestData1() {
return {
profiles: [
{
name: 'Default',
options: createProfileOptionsUpdatedTestData1(),
conditionGroups: [
{
conditions: [
{
type: 'popupLevel',
operator: 'equal',
value: '1'
},
{
type: 'popupLevel',
operator: 'notEqual',
value: '0'
},
{
type: 'popupLevel',
operator: 'lessThan',
value: '3'
},
{
type: 'popupLevel',
operator: 'greaterThan',
value: '0'
},
{
type: 'popupLevel',
operator: 'lessThanOrEqual',
value: '2'
},
{
type: 'popupLevel',
operator: 'greaterThanOrEqual',
value: '1'
}
]
},
{
conditions: [
{
type: 'url',
operator: 'matchDomain',
value: 'example.com'
},
{
type: 'url',
operator: 'matchRegExp',
value: 'example\\.com'
}
]
},
{
conditions: [
{
type: 'modifierKeys',
operator: 'are',
value: 'ctrl, shift'
},
{
type: 'modifierKeys',
operator: 'areNot',
value: 'alt, shift'
},
{
type: 'modifierKeys',
operator: 'include',
value: 'alt'
},
{
type: 'modifierKeys',
operator: 'notInclude',
value: 'ctrl'
}
]
}
]
}
],
profileCurrent: 0,
version: 18,
2020-09-20 15:33:12 +00:00
global: {
database: {
prefixWildcardsSupported: false
}
2020-09-20 15:33:12 +00:00
}
};
}
async function testUpdate(extDir) {
const vm = createVM(extDir);
const [OptionsUtil] = vm.get(['OptionsUtil']);
const optionsUtil = new OptionsUtil();
await optionsUtil.prepare();
const options = createOptionsTestData1();
const optionsUpdated = clone(await optionsUtil.update(options));
const optionsExpected = createOptionsUpdatedTestData1();
assert.deepStrictEqual(optionsUpdated, optionsExpected);
}
async function testDefault(extDir) {
const data = [
(options) => options,
(options) => {
delete options.profiles[0].options.audio.autoPlay;
},
(options) => {
options.profiles[0].options.audio.autoPlay = void 0;
}
];
const vm = createVM(extDir);
const [OptionsUtil] = vm.get(['OptionsUtil']);
const optionsUtil = new OptionsUtil();
await optionsUtil.prepare();
for (const modify of data) {
const options = optionsUtil.getDefault();
const optionsModified = clone(options);
modify(optionsModified);
const optionsUpdated = await optionsUtil.update(clone(optionsModified));
assert.deepStrictEqual(clone(optionsUpdated), clone(options));
}
}
2020-09-20 15:33:12 +00:00
async function testFieldTemplatesUpdate(extDir) {
const vm = createVM(extDir);
const [OptionsUtil, TemplatePatcher] = vm.get(['OptionsUtil', 'TemplatePatcher']);
2020-09-20 15:33:12 +00:00
const optionsUtil = new OptionsUtil();
await optionsUtil.prepare();
const templatePatcher = new TemplatePatcher();
const loadDataFile = (fileName) => {
const content = fs.readFileSync(path.join(extDir, fileName), {encoding: 'utf8'});
return templatePatcher.parsePatch(content).addition;
};
const updates = [
{version: 2, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')},
{version: 4, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')},
{version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')},
{version: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')},
{version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')},
{version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')},
{version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}
];
const getUpdateAdditions = (startVersion, targetVersion) => {
let value = '';
for (const {version, changes} of updates) {
if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; }
if (value.length > 0) { value += '\n'; }
value += changes;
}
return value;
};
2020-09-20 15:33:12 +00:00
const data = [
// Standard format
{
oldVersion: 0,
newVersion: 12,
2020-09-20 15:33:12 +00:00
old: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
2020-09-20 15:33:12 +00:00
{{~> (lookup . "marker") ~}}`.trimStart()
},
// Non-standard marker format
{
oldVersion: 0,
newVersion: 12,
2020-09-20 15:33:12 +00:00
old: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker2") ~}}`.trimStart(),
expected: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker2") ~}}
<<<UPDATE-ADDITIONS>>>`.trimStart()
2020-09-20 15:33:12 +00:00
},
// Empty test
{
oldVersion: 0,
newVersion: 12,
2020-09-20 15:33:12 +00:00
old: `
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
<<<UPDATE-ADDITIONS>>>
2020-09-20 15:33:12 +00:00
{{~> (lookup . "marker") ~}}`.trimStart()
},
// Definition tags update
{
oldVersion: 0,
newVersion: 12,
old: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}
{{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{/inline}}
{{#*inline "glossary-single2"}}
{{~#unless brief~}}
{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}
{{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{/inline}}
{{#*inline "glossary"}}
{{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}}
{{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries~}}
{{/inline}}
{{~> (lookup . "marker") ~}}
`.trimStart(),
expected: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
{{~#set "any" false}}{{/set~}}
{{~#if definitionTags~}}{{#each definitionTags~}}
{{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}}
{{~#if (get "any")}}, {{else}}<i>({{/if~}}
{{name}}
{{~#set "any" true}}{{/set~}}
{{~/if~}}
{{~/each~}}
{{~#if (get "any")}})</i> {{/if~}}
{{~/if~}}
{{~/scope~}}
{{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{/inline}}
{{#*inline "glossary-single2"}}
{{~#unless brief~}}
{{~#scope~}}
{{~#set "any" false}}{{/set~}}
{{~#if definitionTags~}}{{#each definitionTags~}}
{{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}}
{{~#if (get "any")}}, {{else}}<i>({{/if~}}
{{name}}
{{~#set "any" true}}{{/set~}}
{{~/if~}}
{{~/each~}}
{{~#if (get "any")}})</i> {{/if~}}
{{~/if~}}
{{~/scope~}}
{{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{/inline}}
{{#*inline "glossary"}}
{{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}}
{{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries data=../.~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}
`.trimStart()
},
// glossary and glossary-brief update
{
oldVersion: 7,
newVersion: 12,
old: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
{{~#set "any" false}}{{/set~}}
{{~#if definitionTags~}}{{#each definitionTags~}}
{{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}}
{{~#if (get "any")}}, {{else}}<i>({{/if~}}
{{name}}
{{~#set "any" true}}{{/set~}}
{{~/if~}}
{{~/each~}}
{{~#if (get "any")}})</i> {{/if~}}
{{~/if~}}
{{~/scope~}}
{{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{~#if glossary.[1]~}}
{{~#if compactGlossaries~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}}
<ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul>
{{~/if~}}
{{~else~}}
{{~#multiLine}}{{glossary.[0]}}{{/multiLine~}}
{{~/if~}}
{{/inline}}
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{#*inline "glossary"}}
<div style="text-align: left;">
{{~#if modeKanji~}}
{{~#if definition.glossary.[1]~}}
<ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol>
{{~else~}}
{{definition.glossary.[0]}}
{{~/if~}}
{{~else~}}
{{~#if group~}}
{{~#if definition.definitions.[1]~}}
<ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}</li>{{/each}}</ol>
{{~else~}}
{{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}}
{{~/if~}}
{{~else if merge~}}
{{~#if definition.definitions.[1]~}}
<ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}</li>{{/each}}</ol>
{{~else~}}
{{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}}
{{~/if~}}
{{~else~}}
{{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}}
{{~/if~}}
{{~/if~}}
</div>
{{/inline}}
{{#*inline "glossary-brief"}}
{{~> glossary brief=true ~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
{{~#set "any" false}}{{/set~}}
{{~#each definitionTags~}}
{{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}}
{{~#if (get "any")}}, {{else}}<i>({{/if~}}
{{name}}
{{~#set "any" true}}{{/set~}}
{{~/if~}}
{{~/each~}}
{{~#unless noDictionaryTag~}}
{{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}}
{{~#if (get "any")}}, {{else}}<i>({{/if~}}
{{dictionary}}
{{~#set "any" true}}{{/set~}}
{{~/if~}}
{{~/unless~}}
{{~#if (get "any")}})</i> {{/if~}}
{{~/scope~}}
{{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}}
{{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}}
{{~else if @root.compactGlossaries~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}}
<ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul>
{{~/if~}}
{{~#set "previousDictionary" dictionary~}}{{~/set~}}
{{/inline}}
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~#*inline "glossary"~}}
<div style="text-align: left;">
{{~#scope~}}
{{~#if (op "===" definition.type "term")~}}
{{~> glossary-single definition brief=brief noDictionaryTag=noDictionaryTag ~}}
{{~else if (op "||" (op "===" definition.type "termGrouped") (op "===" definition.type "termMerged"))~}}
{{~#if (op ">" definition.definitions.length 1)~}}
<ol>{{~#each definition.definitions~}}<li>{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}</li>{{~/each~}}</ol>
{{~else~}}
{{~#each definition.definitions~}}{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}{{~/each~}}
{{~/if~}}
{{~else if (op "===" definition.type "kanji")~}}
{{~#if (op ">" definition.glossary.length 1)~}}
<ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol>
{{~else~}}
{{~#each definition.glossary~}}{{.}}{{~/each~}}
{{~/if~}}
{{~/if~}}
{{~/scope~}}
</div>
{{~/inline~}}
{{#*inline "glossary-no-dictionary"}}
{{~> glossary noDictionaryTag=true ~}}
{{/inline}}
{{#*inline "glossary-brief"}}
{{~> glossary brief=true ~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
},
// formatGlossary update
{
oldVersion: 12,
newVersion: 13,
old: `
{{#*inline "example"}}
{{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}}
{{~else if @root.compactGlossaries~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}}
<ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul>
{{~/if~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
{{#*inline "example"}}
{{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}
{{~else if @root.compactGlossaries~}}
{{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}}
<ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>
{{~/if~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
},
// hasMedia/getMedia update
{
oldVersion: 12,
newVersion: 13,
old: `
{{#*inline "audio"}}
{{~#if definition.audioFileName~}}
[sound:{{definition.audioFileName}}]
{{~/if~}}
{{/inline}}
{{#*inline "screenshot"}}
<img src="{{definition.screenshotFileName}}" />
{{/inline}}
{{#*inline "clipboard-image"}}
{{~#if definition.clipboardImageFileName~}}
<img src="{{definition.clipboardImageFileName}}" />
{{~/if~}}
{{/inline}}
{{#*inline "clipboard-text"}}
{{~#if definition.clipboardText~}}{{definition.clipboardText}}{{~/if~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
{{#*inline "audio"}}
{{~#if (hasMedia "audio")~}}
[sound:{{#getMedia "audio"}}{{/getMedia}}]
{{~/if~}}
{{/inline}}
{{#*inline "screenshot"}}
{{~#if (hasMedia "screenshot")~}}
<img src="{{#getMedia "screenshot"}}{{/getMedia}}" />
{{~/if~}}
{{/inline}}
{{#*inline "clipboard-image"}}
{{~#if (hasMedia "clipboardImage")~}}
<img src="{{#getMedia "clipboardImage"}}{{/getMedia}}" />
{{~/if~}}
{{/inline}}
{{#*inline "clipboard-text"}}
{{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
},
// hasMedia/getMedia update
{
oldVersion: 12,
newVersion: 13,
old: `
{{! Pitch Accents }}
{{#*inline "pitch-accent-item-downstep-notation"}}
{{~#scope~}}
<span>
{{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}}
{{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}}
{{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}}
{{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}}
{{~#each (getKanaMorae reading)~}}
{{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}}
{{~#set "style2"}}{{/set~}}
{{~#if (isMoraPitchHigh @index ../position)}}
{{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}}
{{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}}
{{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}}
{{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}}
{{~/if~}}
{{~/if~}}
<span style="{{#get "style1"}}{{/get}}">{{{.}}}<span style="{{#get "style2"}}{{/get}}"></span></span>
{{~/each~}}
</span>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}}
{{#*inline "pitch-accent-item-graph"}}
{{~#scope~}}
{{~#set "morae" (getKanaMorae reading)}}{{/set~}}
{{~#set "morae-count" (property (get "morae") "length")}}{{/set~}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {{#op "+" 50 (op "*" 50 (get "morae-count"))}}{{/op}} 100" style="display:inline-block;height:2em;">
<defs>
<g id="term-pitch-accent-graph-dot"><circle cx="0" cy="0" r="15" style="fill:#000;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-dot-downstep"><circle cx="0" cy="0" r="15" style="fill:none;stroke:#000;stroke-width:5;" /><circle cx="0" cy="0" r="5" style="fill:none;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-triangle"><path d="M0 13 L15 -13 L-15 -13 Z" style="fill:none;stroke:#000;stroke-width:5;" /></g>
</defs>
<path style="fill:none;stroke:#000;stroke-width:5;" d="
{{~#set "cmd" "M"}}{{/set~}}
{{~#each (get "morae")~}}
{{~#get "cmd"}}{{/get~}}
{{~> pitch-accent-item-graph-position index=@index position=../position~}}
{{~#set "cmd" "L"}}{{/set~}}
{{~/each~}}
"></path>
<path style="fill:none;stroke:#000;stroke-width:5;stroke-dasharray:5 5;" d="M{{> pitch-accent-item-graph-position index=(op "-" (get "morae-count") 1) position=position}} L{{> pitch-accent-item-graph-position index=(get "morae-count") position=position}}"></path>
{{#each (get "morae")}}
<use href="{{#if (op "&&" (isMoraPitchHigh @index ../position) (op "!" (isMoraPitchHigh (op "+" @index 1) ../position)))}}#term-pitch-accent-graph-dot-downstep{{else}}#term-pitch-accent-graph-dot{{/if}}" x="{{> pitch-accent-item-graph-position-x index=@index position=../position}}" y="{{> pitch-accent-item-graph-position-y index=@index position=../position}}"></use>
{{/each}}
<use href="#term-pitch-accent-graph-triangle" x="{{> pitch-accent-item-graph-position-x index=(get "morae-count") position=position}}" y="{{> pitch-accent-item-graph-position-y index=(get "morae-count") position=position}}"></use>
</svg>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-position"~}}
<span>[{{position}}]</span>
{{~/inline}}
{{#*inline "pitch-accent-item"}}
{{~#if (op "==" format "downstep-notation")~}}
{{~> pitch-accent-item-downstep-notation~}}
{{~else if (op "==" format "graph")~}}
{{~> pitch-accent-item-graph~}}
{{~else if (op "==" format "position")~}}
{{~> pitch-accent-item-position~}}
{{~/if~}}
{{/inline}}
{{#*inline "pitch-accent-item-disambiguation"}}
{{~#scope~}}
{{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}}
{{~#if (op ">" (property (get "exclusive") "length") 0)~}}
{{~#set "separator" ""~}}{{/set~}}
<em>({{#each (get "exclusive")~}}
{{~#get "separator"}}{{/get~}}{{{.}}}
{{~/each}} only) </em>
{{~/if~}}
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-list"}}
{{~#if (op ">" pitchCount 0)~}}
{{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}}
{{~#each pitches~}}
{{~#each pitches~}}
{{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}}
{{~> pitch-accent-item-disambiguation~}}
{{~> pitch-accent-item format=../../format~}}
{{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}}
{{~/each~}}
{{~/each~}}
{{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}}
{{~else~}}
No pitch accent data
{{~/if~}}
{{/inline}}
{{#*inline "pitch-accents"}}
{{~> pitch-accent-list format='downstep-notation'~}}
{{/inline}}
{{#*inline "pitch-accent-graphs"}}
{{~> pitch-accent-list format='graph'~}}
{{/inline}}
{{#*inline "pitch-accent-positions"}}
{{~> pitch-accent-list format='position'~}}
{{/inline}}
{{! End Pitch Accents }}
{{~> (lookup . "marker") ~}}`.trimStart(),
expected: `
{{! Pitch Accents }}
{{#*inline "pitch-accent-item"}}
{{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}}
{{/inline}}
{{#*inline "pitch-accent-item-disambiguation"}}
{{~#scope~}}
{{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}}
{{~#if (op ">" (property (get "exclusive") "length") 0)~}}
{{~#set "separator" ""~}}{{/set~}}
<em>({{#each (get "exclusive")~}}
{{~#get "separator"}}{{/get~}}{{{.}}}
{{~/each}} only) </em>
{{~/if~}}
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-list"}}
{{~#if (op ">" pitchCount 0)~}}
{{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}}
{{~#each pitches~}}
{{~#each pitches~}}
{{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}}
{{~> pitch-accent-item-disambiguation~}}
{{~> pitch-accent-item format=../../format~}}
{{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}}
{{~/each~}}
{{~/each~}}
{{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}}
{{~else~}}
No pitch accent data
{{~/if~}}
{{/inline}}
{{#*inline "pitch-accents"}}
{{~> pitch-accent-list format='text'~}}
{{/inline}}
{{#*inline "pitch-accent-graphs"}}
{{~> pitch-accent-list format='graph'~}}
{{/inline}}
{{#*inline "pitch-accent-positions"}}
{{~> pitch-accent-list format='position'~}}
{{/inline}}
{{! End Pitch Accents }}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
2020-09-20 15:33:12 +00:00
}
];
const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g;
for (const {old, expected, oldVersion, newVersion} of data) {
2020-09-20 15:33:12 +00:00
const options = createOptionsTestData1();
options.profiles[0].options.anki.fieldTemplates = old;
options.version = oldVersion;
const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion));
const optionsUpdated = clone(await optionsUtil.update(options, newVersion));
2020-09-20 15:33:12 +00:00
const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates;
assert.deepStrictEqual(fieldTemplatesActual, expected2);
2020-09-20 15:33:12 +00:00
}
}
async function main() {
const extDir = path.join(__dirname, '..', 'ext');
await testUpdate(extDir);
await testDefault(extDir);
2020-09-20 15:33:12 +00:00
await testFieldTemplatesUpdate(extDir);
}
if (require.main === module) { testMain(main); }