Spell checker improvements

- Add/Remove Dictionaries window
- Better account for the (unlikely) possibility that a dictionary could
  be replaced by another more popular dictionary provided by a different
  extension id (tested)
- Better account for the (very unlikely) possibility that an extension
  could bundle multiple dictionaries (untested)
- Use toolkit version comparator for proper extension version
  comparisons
- Localize strings
- Add tests for updating
This commit is contained in:
Dan Stillman 2021-06-22 20:58:16 -04:00
parent 7f2296b1fb
commit 9a7016ad64
9 changed files with 583 additions and 85 deletions

View file

@ -0,0 +1,193 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2021 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
// eslint-disable-next-line camelcase, no-unused-vars
var Zotero_Dictionary_Manager = new function () {
const HTML_NS = 'http://www.w3.org/1999/xhtml';
var installed;
var updateMap;
this.init = async function () {
document.title = Zotero.getString('spellCheck.dictionaryManager.title');
installed = new Set(Zotero.Dictionaries.dictionaries.map(d => d.id));
var installedLocales = new Set(Zotero.Dictionaries.dictionaries.map(d => d.locale));
var availableDictionaries = await Zotero.Dictionaries.fetchDictionariesList();
var availableUpdates = await Zotero.Dictionaries.getAvailableUpdates(availableDictionaries);
updateMap = new Map(availableUpdates.map(x => [x.old.id, x.new.id]));
var { InlineSpellChecker } = ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm", {});
var isc = new InlineSpellChecker();
// Start with installed dictionaries
var list = [];
for (let d of Zotero.Dictionaries.dictionaries) {
let name = Zotero.Dictionaries.getBestDictionaryName(d.locale, isc);
list.push(Object.assign({}, d, { name }));
}
// Add remote dictionaries not in the list
for (let d of availableDictionaries) {
if (!installed.has(d.id) && !installedLocales.has(d.locale)) {
list.push(d);
}
}
var positionMap = new Map(availableDictionaries.map((d, i) => [d.locale, i + 1]));
list.sort((a, b) => {
// If both locales are in original list, use the original sort order
let posA = positionMap.get(a.locale);
let posB = positionMap.get(b.locale);
if (posA && posB) {
return posA - posB;
}
// Otherwise compare the locale codes
return Zotero.localeCompare(a.locale, b.locale);
});
// Build list
var listbox = document.getElementById('dictionaries');
for (let d of list) {
let name = d.name;
let li = document.createElement('richlistitem');
let div = document.createElementNS(HTML_NS, 'div');
let checkbox = document.createElementNS(HTML_NS, 'input');
checkbox.type = 'checkbox';
checkbox.id = d.locale;
// Store properties on element
// .id will be the current id for installed dictionaries and otherwise the remote id
checkbox.dataset.dictId = d.id;
checkbox.dataset.dictLocale = d.locale;
checkbox.dataset.dictName = d.name;
// en-US is always checked and disabled
checkbox.checked = d.locale == 'en-US' || installed.has(d.id);
if (d.locale == 'en-US') {
checkbox.disabled = true;
}
checkbox.setAttribute('tabindex', -1);
let label = document.createElementNS(HTML_NS, 'label');
label.setAttribute('for', d.locale);
// Add " (update available)"
if (updateMap.has(d.id)) {
name = Zotero.getString('spellCheck.dictionaryManager.updateAvailable', name);
}
label.textContent = name;
// Don't toggle checkbox for single-click on label
label.onclick = (event) => {
if (event.detail == 1) {
event.preventDefault();
}
};
div.appendChild(checkbox);
div.appendChild(label);
li.appendChild(div);
listbox.appendChild(li);
}
listbox.selectedIndex = 0;
};
this.handleAccept = async function () {
// Download selected dictionaries if updated or not currently installed
var elems = document.querySelectorAll('input[type=checkbox]');
var toRemove = [];
var toDownload = [];
for (let elem of elems) {
if (elem.dataset.dictLocale == 'en-US') {
continue;
}
let id = elem.dataset.dictId;
if (!elem.checked) {
if (installed.has(id)) {
toRemove.push(id);
}
continue;
}
if (updateMap.has(id)) {
// If id is changing, delete the old one first
toRemove.push(id);
toDownload.push({ id: updateMap.get(id), name: elem.dataset.dictName });
}
else if (!installed.has(id)) {
toDownload.push({ id, name: elem.dataset.dictName });
}
}
if (toRemove.length) {
for (let id of toRemove) {
await Zotero.Dictionaries.remove(id);
}
}
if (toDownload.length) {
for (let { id, name } of toDownload) {
_updateStatus(Zotero.getString('general.downloading.quoted', name));
try {
await Zotero.Dictionaries.install(id);
}
catch (e) {
Zotero.logError(e);
Zotero.alert(
null,
Zotero.getString('general.error'),
Zotero.getString('spellCheck.dictionaryManager.error.unableToInstall', name)
+ "\n\n" + e.message
);
return;
}
finally {
_updateStatus();
}
}
}
window.close();
};
function _updateStatus(msg) {
var elem = document.getElementById('status');
elem.textContent = msg
// Use non-breaking space to maintain height when empty
|| '\xA0';
}
};
window.addEventListener('keypress', function (event) {
// Toggle checkbox on spacebar
if (event.key == ' ') {
if (event.target.localName == 'richlistbox') {
let elem = event.target.selectedItem.querySelector('input[type=checkbox]');
if (!elem.disabled) {
elem.checked = !elem.checked;
}
}
}
if (event.key == 'Enter') {
document.querySelector('button[dlgtype="accept"]').click();
}
});

View file

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
<window
id="dictionary-manager"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="Zotero_Dictionary_Manager.init()"
windowtype="zotero:dictionaries"
width="445"
height="400">
<script src="chrome://global/content/globalOverlay.js"/>
<script src="include.js"/>
<script src="commonDialog.js"/>
<script src="dictionaryManager.js"/>
<vbox id="DictionaryManager" flex="1">
<richlistbox id="dictionaries" flex="1" tabindex="0"/>
<html:div id="status">&#xA0;</html:div>
<hbox class="dialog-button-box">
<button dlgtype="cancel" label="&zotero.general.cancel;" oncommand="window.close()"/>
<button dlgtype="accept" default="true" label="&zotero.general.ok;"
oncommand="Zotero_Dictionary_Manager.handleAccept()"/>
</hbox>
</vbox>
<keyset>
<key id="key_close" key="W" modifiers="accel" oncommand="window.close()"/>
</keyset>
</window>

View file

@ -39,6 +39,7 @@ Zotero.Dictionaries = new function () {
}
});
// Note: Doesn't include bundled en-US
Zotero.defineProperty(this, 'dictionaries', {
get: () => {
return _dictionaries;
@ -63,7 +64,7 @@ Zotero.Dictionaries = new function () {
}
try {
let dir = OS.Path.join(dictionariesDir, entry.name);
await _loadDictionary(dir);
await _loadDirectory(dir);
}
catch (e) {
Zotero.logError(e);
@ -86,41 +87,6 @@ Zotero.Dictionaries = new function () {
return req.response;
};
/**
* Install the most popular dictionary for specified locale
*
* @param locale
* @return {Promise<Boolean>}
*/
this.installByLocale = async function (locale) {
let dictionaries = await this.fetchDictionariesList();
let matched = dictionaries.filter(x => x.locale === locale);
if (!matched.length) {
matched = dictionaries.filter(x => x.locale === locale.split(/[-_]/)[0]);
}
if (!matched.length) {
return false;
}
matched.sort((a, b) => b.users - a.users);
await this.install(matched[0].id);
return true;
};
/**
* Remove all dictionaries targeting specific locale
*
* @param locale
* @return {Promise}
*/
this.removeByLocale = async function(locale) {
for (let dictionary of _dictionaries) {
if (dictionary.locales.includes(locale)
|| dictionary.locales.some(x => x === locale.split(/[-_]/)[0])) {
await this.remove(dictionary.id);
}
}
};
/**
* Install dictionary by extension id,
* e.g., `en-NZ@dictionaries.addons.mozilla.org`
@ -129,7 +95,11 @@ Zotero.Dictionaries = new function () {
* @return {Promise}
*/
this.install = async function (id) {
if (id == '@unitedstatesenglishdictionary') {
throw new Error("en-US dictionary is bundled");
}
await this.remove(id);
Zotero.debug("Installing dictionaries from " + id);
let url = this.baseURL + id + '.xpi';
let xpiPath = OS.Path.join(Zotero.getTempDirectory().path, id);
let dir = OS.Path.join(Zotero.Profile.dir, 'dictionaries', id);
@ -162,7 +132,7 @@ Zotero.Dictionaries = new function () {
zipReader.close();
await OS.File.remove(xpiPath);
await _loadDictionary(dir);
await _loadDirectory(dir);
}
catch (e) {
if (await OS.File.exists(xpiPath)) {
@ -176,46 +146,116 @@ Zotero.Dictionaries = new function () {
};
/**
* Remove dictionary by extension id
* Remove dictionaries by extension id
*
* @param {String} id
* @return {Promise}
*/
this.remove = async function (id) {
let dictionaryIndex = _dictionaries.findIndex(x => x.id === id);
if (dictionaryIndex !== -1) {
let dictionary = _dictionaries[dictionaryIndex];
try {
let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json');
let manifest = await Zotero.File.getContentsAsync(manifestPath);
manifest = JSON.parse(manifest);
for (let locale in manifest.dictionaries) {
let dicPath = manifest.dictionaries[locale];
let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff');
_spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
Zotero.debug("Removing dictionaries from " + id);
var dictionary = _dictionaries.find(x => x.id === id);
if (!dictionary) {
return;
}
try {
let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json');
let manifest = await Zotero.File.getContentsAsync(manifestPath);
manifest = JSON.parse(manifest);
for (let locale in manifest.dictionaries) {
let dicPath = manifest.dictionaries[locale];
let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff');
Zotero.debug(`Removing ${locale} dictionary`);
_spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
}
}
catch (e) {
Zotero.logError(e);
}
await OS.File.removeDir(dictionary.dir);
// Technically there can be more than one dictionary provided by the same extension id,
// so remove all that match
_dictionaries = _dictionaries.filter(x => x.id != id);
};
/**
* @param {Object[]} [dictionaries] - Dictionary list from fetchDictionariesList(); fetched
* automatically if not provided
* @return {Object[]} - Array of objects with 'old' and 'new'
*/
this.getAvailableUpdates = async function (dictionaries) {
var updates = [];
let availableDictionaries = dictionaries || await this.fetchDictionariesList();
for (let dictionary of _dictionaries) {
let availableDictionary = availableDictionaries.find((x) => {
return x.id === dictionary.id || x.locale == dictionary.locale;
});
if (!availableDictionary) continue;
// If same id, check if version is higher
if (availableDictionary.id == dictionary.id) {
if (Services.vc.compare(dictionary.version, availableDictionary.version) < 0) {
updates.push({ old: dictionary, new: availableDictionary });
}
}
// If different id for same locale, always offer as an update
else {
updates.push({ old: dictionary, new: availableDictionary });
}
}
if (updates.length) {
Zotero.debug("Available dictionary updates:");
Zotero.debug(updates);
}
else {
Zotero.debug("No dictionary updates found");
}
return updates;
};
/**
* Get the best display name for a dictionary
*
* For known locales, this will be the native name in the target locale. If a native name isn't
* available and inlineSpellChecker is provided, an English name will be provided if available.
*
* @param {String} locale
* @param {InlineSpellChecker} [inlineSpellChecker] - An instance of InlineSpellChecker from
* InlineSpellChecker.jsm
* @return {String} - The best available name, or the locale code if unavailable
*/
this.getBestDictionaryName = function (locale, inlineSpellChecker) {
var name = Zotero.Locale.availableLocales[locale];
if (!name) {
for (let key in Zotero.Locale.availableLocales) {
if (key.split('-')[0] === locale) {
name = Zotero.Locale.availableLocales[key];
}
}
}
if (!name && inlineSpellChecker) {
name = inlineSpellChecker.getDictionaryDisplayName(locale)
}
return name || name;
};
/**
* Update dictionaries
*
* @return {Promise<Integer>} - Number of updated dictionaries
*/
this.update = async function () {
var updates = await Zotero.Dictionaries.getAvailableUpdates();
var updated = 0;
for (let update of updates) {
try {
await this.remove(update.old.id);
await this.install(update.new.id);
updated++;
}
catch (e) {
Zotero.logError(e);
}
await OS.File.removeDir(dictionary.dir);
_dictionaries.splice(dictionaryIndex, 1);
}
};
/**
* Update all dictionaries
*
* @return {Promise}
*/
this.update = async function () {
let availableDictionaries = await this.fetchDictionariesList();
for (let dictionary of _dictionaries) {
let availableDictionary = availableDictionaries.find(x => x.id === dictionary.id);
if (availableDictionary && availableDictionary.version > dictionary.version) {
await this.install(availableDictionary.id);
}
}
return updated;
};
/**
@ -224,7 +264,7 @@ Zotero.Dictionaries = new function () {
* @param {String} dir
* @return {Promise}
*/
async function _loadDictionary(dir) {
async function _loadDirectory(dir) {
let manifestPath = OS.Path.join(dir, 'manifest.json');
let manifest = await Zotero.File.getContentsAsync(manifestPath);
manifest = JSON.parse(manifest);
@ -241,8 +281,9 @@ Zotero.Dictionaries = new function () {
locales.push(locale);
let dicPath = manifest.dictionaries[locale];
let affPath = OS.Path.join(dir, dicPath.slice(0, -3) + 'aff');
Zotero.debug(`Adding ${locale} dictionary`);
_spellChecker.addDictionary(locale, Zotero.File.pathToFile(affPath));
_dictionaries.push({ id, locale, version, dir });
}
_dictionaries.push({ id, locales, version, dir });
}
};

View file

@ -806,8 +806,6 @@ class EditorInstance {
appendItems(this._popup, itemGroups);
// TODO: Localize
// Spell checker
let spellChecker = this._getSpellChecker();
@ -816,7 +814,7 @@ class EditorInstance {
this._popup.appendChild(separator);
// Check Spelling
var menuitem = this._popup.ownerDocument.createElement('menuitem');
menuitem.setAttribute('label', 'Check Spelling');
menuitem.setAttribute('label', Zotero.getString('spellCheck.checkSpelling'));
menuitem.setAttribute('checked', spellChecker.enabled);
menuitem.addEventListener('command', () => {
// Possible values: 0 - off, 1 - only multi-line, 2 - multi and single line input boxes
@ -827,7 +825,7 @@ class EditorInstance {
if (spellChecker.enabled) {
// Languages menu
var menu = this._popup.ownerDocument.createElement('menu');
menu.setAttribute('label', 'Languages');
menu.setAttribute('label', Zotero.getString('general.languages'));
this._popup.append(menu);
// Languages menu popup
var menupopup = this._popup.ownerDocument.createElement('menupopup');
@ -835,19 +833,13 @@ class EditorInstance {
spellChecker.addDictionaryListToMenu(menupopup, null);
// The menu is prepopulated with names from InlineSpellChecker::getDictionaryDisplayName(),
// which will be in English, so swap in native locale names where we have them
for (var menuitem of menupopup.children) {
// 'spell-check-dictionary-en-US'
let locale = menuitem.id.slice(23);
let label = Zotero.Locale.availableLocales[locale];
if (!label) {
for(let key in Zotero.Locale.availableLocales) {
if (key.split('-')[0] === locale) {
label = Zotero.Locale.availableLocales[key];
}
}
}
if (label) {
let label = Zotero.Dictionaries.getBestDictionaryName(locale);
if (label && label != locale) {
menuitem.setAttribute('label', label);
}
}
@ -857,9 +849,11 @@ class EditorInstance {
menupopup.appendChild(separator);
// Add Dictionaries
var menuitem = this._popup.ownerDocument.createElement('menuitem');
menuitem.setAttribute('label', 'Add Dictionaries...');
menuitem.setAttribute('label', Zotero.getString('spellCheck.addRemoveDictionaries'));
menuitem.addEventListener('command', () => {
Zotero.Utilities.Internal.openPreferences('zotero-prefpane-advanced');
Services.ww.openWindow(null, "chrome://zotero/content/dictionaryManager.xul",
"dictionary-manager", "chrome,centerscreen", {});
});
menupopup.append(menuitem);

View file

@ -1,6 +1,8 @@
Zotero.Locale = {
/**
* Keep this up to date with chrome.manifest
* Keep this up to date with chrome.manifest and zotero-build/dictionaries/build-dictionaries
*
* Names from https://addons.mozilla.org/en-US/firefox/language-tools/
*/
availableLocales: Object.freeze({
'ar': 'عربي',

View file

@ -26,6 +26,8 @@ general.checkForUpdates = Check for Updates
general.actionCannotBeUndone = This action cannot be undone.
general.install = Install
general.updateAvailable = Update Available
general.downloading = Downloading %S…
general.downloading.quoted = Downloading “%S”…
general.noUpdatesFound = No Updates Found
general.isUpToDate = %S is up to date.
general.upgrade = Upgrade
@ -79,6 +81,7 @@ general.nMegabytes = %S MB
general.item = Item
general.pdf = PDF
general.back = Back
general.languages = Languages
general.yellow = Yellow
general.red = Red
@ -1342,3 +1345,9 @@ pdfReader.nextPage = Next Page
pdfReader.previousPage = Previous Page
pdfReader.page = Page
pdfReader.readOnly = Read-only
spellCheck.checkSpelling = Check Spelling
spellCheck.addRemoveDictionaries = Add/Remove Dictionaries…
spellCheck.dictionaryManager.title = Add/Remove Dictionaries
spellCheck.dictionaryManager.updateAvailable = %S (update available)
spellCheck.dictionaryManager.error.unableToInstall = Unable to install “%S”:

View file

@ -25,6 +25,7 @@
@import "components/autosuggest";
@import "components/button";
@import "components/createParent";
@import "components/dictionaryManager";
@import "components/editable";
@import "components/exportOptions";
@import "components/icons";

View file

@ -0,0 +1,27 @@
#DictionaryManager {
padding: 15px;
richlistitem {
font-size: 12px;
}
input {
// Select rows, not checkboxes
-moz-user-focus: ignore;
}
#status {
margin: 9px 0 0 5px;
font-size: 10px;
line-height: 1em;
}
.dialog-button-box {
display: flex;
justify-content: end;
}
button {
font-size: 13px;
}
}

View file

@ -0,0 +1,193 @@
"use strict";
var sandbox = sinon.createSandbox();
describe("Dictionaries", function () {
var win;
var enUKXPIOld, frFRv1XPI, unKNXPI, enUKXPINew, frFRv2XPI;
async function makeFakeDictionary({ id, locale, version }) {
var dir = await getTempDirectory();
var extDir = OS.Path.join(dir, 'sub');
var dictDir = OS.Path.join(extDir, 'dictionaries');
await OS.File.makeDir(dictDir, { from: dir });
var manifest = {
dictionaries: {
[locale]: `dictionaries/${locale}.dic`,
},
version,
applications: {
gecko: {
id
}
},
name,
manifest_version: 2
};
await Zotero.File.putContentsAsync(
OS.Path.join(extDir, 'manifest.json'),
JSON.stringify(manifest)
);
await Zotero.File.putContentsAsync(
OS.Path.join(dictDir, locale + '.dic'),
"1\n0/nm"
);
var path = OS.Path.join(dir, id + '.xpi');
await Zotero.File.zipDirectory(extDir, path);
return path;
}
before(async function () {
// Make fake installed dictionaries
enUKXPIOld = await makeFakeDictionary({
id: '@fake-en-UK-dictionary',
locale: 'en-UK',
version: 5,
name: "Fake English UK Dictionary"
});
frFRv1XPI = await makeFakeDictionary({
id: '@fake-fr-FR-dictionary',
locale: 'fr-FR',
version: 1,
name: "Fake French Dictionary"
});
unKNXPI = await makeFakeDictionary({
id: '@fake-unknown-dictionary',
locale: 'xx-UN',
version: 5,
name: "Fake Unknown Dictionary"
});
// Make fake updated dictionaries
enUKXPINew = await makeFakeDictionary({
id: '@another-fake-en-UK-dictionary',
locale: 'en-UK',
version: 1,
name: "Another Fake English UK Dictionary"
});
frFRv2XPI = await makeFakeDictionary({
id: '@fake-fr-FR-dictionary',
locale: 'fr-FR',
version: 2,
name: "Fake French Dictionary"
});
});
beforeEach(async function () {
for (let id of Zotero.Dictionaries.dictionaries.map(x => x.id)) {
await Zotero.Dictionaries.remove(id);
}
sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
if (url.includes('en-UK')) {
return OS.File.copy(enUKXPIOld, downloadPath);
}
if (url.includes('fr-FR')) {
return OS.File.copy(frFRv1XPI, downloadPath);
}
if (url.includes('xx-UN')) {
return OS.File.copy(unKNXPI, downloadPath);
}
throw new Error("Unexpected URL " + url);
});
await Zotero.Dictionaries.install('@fake-en-UK-dictionary');
await Zotero.Dictionaries.install('@fake-fr-FR-dictionary');
await Zotero.Dictionaries.install('@fake-xx-UN-dictionary');
sandbox.restore();
// Create metadata response for available dictionaries
sandbox.stub(Zotero.Dictionaries, 'fetchDictionariesList')
.resolves([
{
id: '@another-fake-en-UK-dictionary',
locale: 'en-UK',
name: "English (UK)",
version: 1
},
{
id: '@fake-fr-FR-dictionary',
locale: 'fr-FR',
name: "Français",
version: 2
}
]);
sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
if (url.includes('en-UK')) {
return OS.File.copy(enUKXPINew, downloadPath);
}
if (url.includes('fr-FR')) {
return OS.File.copy(frFRv2XPI, downloadPath);
}
throw new Error("Unexpected URL " + url);
});
});
afterEach(function () {
sandbox.restore();
});
describe("Zotero.Dictionaries", function () {
describe("#update()", function () {
it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
var numDictionaries = Zotero.Dictionaries.dictionaries.length;
function updated() {
return !!(
!Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
// Version update happens too
&& !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
);
}
assert.isFalse(updated());
await Zotero.Dictionaries.update();
assert.isTrue(updated());
assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
});
});
});
describe("Dictionary Manager", function () {
beforeEach(async function () {
win = Services.ww.openWindow(
null,
'chrome://zotero/content/dictionaryManager.xul',
'dictionary-manager',
'chrome,centerscreen',
{}
);
while (!win.document.querySelectorAll('input[type="checkbox"]').length) {
await Zotero.Promise.delay(50);
}
});
afterEach(function () {
win.close();
});
it("should show unknown dictionary as installed", async function () {
var elems = win.document.querySelectorAll('input[type="checkbox"]');
var names = [...elems].map(elem => elem.dataset.dictName);
assert.sameMembers(names, ['English (UK)', 'Français', 'xx (UN)']);
});
it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
var numDictionaries = Zotero.Dictionaries.dictionaries.length;
function updated() {
return !!(
!Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
// Version update happens too
&& !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
&& Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
);
}
assert.isFalse(updated());
win.document.querySelector('button[dlgtype="accept"]').click();
while (!updated()) {
await Zotero.Promise.delay(50);
}
assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
});
});
});