Skip to content

Commit

Permalink
✨ native dictionary optionally used for spell checking functions
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa committed Sep 18, 2022
1 parent c2e87d9 commit 9794439
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 76 deletions.
125 changes: 116 additions & 9 deletions src/spell-check/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ describe('Spell-check module test suite', () => {
let mockSettings;
let spellCheck;
beforeEach(() => {
jest.resetModules();
mockBrowserWindow = {
destroy: jest.fn(),
loadURL: jest.fn()
};
mockIpc = {
handle: jest.fn()
handle: jest.fn(),
removeHandler: jest.fn()
};
mockSettings = {
enabledDictionaries: []
};
jest.resetModules();
jest.mock('electron', () => ({
BrowserWindow: jest.fn(() => mockBrowserWindow),
ipcMain: mockIpc
ipcMain: mockIpc,
MenuItem: jest.fn(({label, click}) => ({label, click}))
}));
jest.mock('../../settings', () => ({
loadSettings: jest.fn(() => mockSettings)
Expand All @@ -46,11 +49,115 @@ describe('Spell-check module test suite', () => {
// Then
expect(result).toEqual(['13-37']);
});
test('loadDictionaries', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockBrowserWindow.loadURL).toHaveBeenCalledWith(expect.stringMatching(/\/dictionary.renderer\/index.html$/));
expect(mockIpc.handle).toHaveBeenCalledWith('dictionaryGetMisspelled', expect.any(Function));
describe('loadDictionaries', () => {
test('should destroy existing previous fakeRenderer', () => {
// Given
spellCheck.loadDictionaries();
// When
spellCheck.loadDictionaries();
// Then
expect(mockBrowserWindow.destroy).toHaveBeenCalledTimes(1);
});
test('should not destroy non-existing previous fakeRenderer', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockBrowserWindow.destroy).not.toHaveBeenCalled();
});
test('should remove and then add handler', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockIpc.handle).toHaveBeenCalledAfter(mockIpc.removeHandler);
});
test('should remove dictionaryGetMisspelled handler', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockIpc.removeHandler).toHaveBeenCalledWith('dictionaryGetMisspelled');
});
test('should handle dictionaryGetMisspelled', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockIpc.handle).toHaveBeenCalledWith('dictionaryGetMisspelled', expect.any(Function));
});
test('should load dictionary.renderer URL', () => {
// When
spellCheck.loadDictionaries();
// Then
expect(mockBrowserWindow.loadURL)
.toHaveBeenCalledWith(expect.stringMatching(/\/dictionary.renderer\/index.html$/));
});
});
describe('Context Menu handlers', () => {
let params;
let webContents;
beforeEach(() => {
params = {};
webContents = {};
mockBrowserWindow.webContents = webContents;
spellCheck.loadDictionaries();
});
describe('contextMenuHandler', () => {
test('with no misspelled word, should return empty array', async () => {
// When
const result = await spellCheck.contextMenuHandler({}, params, webContents);
// Then
expect(result).toEqual([]);
});
describe('with misspelled word', () => {
beforeEach(() => {
params.misspelledWord = 'the-word';
});
test('and no suggestions, should return empty array', async () => {
// Given
mockBrowserWindow.webContents.executeJavaScript = jest.fn(async () => []);
// When
const result = await spellCheck.contextMenuHandler({}, params, webContents);
// Then
expect(result).toEqual([]);
});
test('and suggestions, should return array of MenuItems', async () => {
// Given
mockBrowserWindow.webContents.executeJavaScript = jest.fn(async () => ['the-suggestion']);
// When
const result = await spellCheck.contextMenuHandler({}, params, webContents);
// Then
expect(result).toEqual([
expect.objectContaining({label: 'the-suggestion'})
]);
});
});
});
describe('contextMenuNativeHandler', () => {
test('with no misspelled word, should return empty array', () => {
// When
const result = spellCheck.contextMenuNativeHandler({}, params, webContents);
// Then
expect(result).toEqual([]);
});
describe('with misspelled word', () => {
beforeEach(() => {
params.misspelledWord = 'the-word';
});
test('and no suggestions, should return empty array', () => {
// When
const result = spellCheck.contextMenuNativeHandler({}, params, webContents);
// Then
expect(result).toEqual([]);
});
test('and suggestions, should return array of MenuItems', async () => {
// Given
params.dictionarySuggestions = ['the-suggestion'];
// When
const result = spellCheck.contextMenuNativeHandler({}, params, webContents);
// Then
expect(result).toEqual([
expect.objectContaining({label: 'the-suggestion'})
]);
});
});
});
});
});
52 changes: 32 additions & 20 deletions src/spell-check/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
const {BrowserWindow, ipcMain} = require('electron');
const {BrowserWindow, MenuItem, ipcMain} = require('electron');
const {APP_EVENTS} = require('../constants');
const {loadSettings} = require('../settings');

Expand Down Expand Up @@ -87,48 +87,60 @@ const getAvailableNativeDictionaries = () =>
const handleGetMisspelled = async (_event, words) =>
fakeRendererWorker.webContents.executeJavaScript(`getMisspelled(${JSON.stringify(words)})`);

const getUseNativeSpellChecker = () => loadSettings().useNativeSpellChecker;

const getEnabledDictionaries = () => loadSettings().enabledDictionaries;

const loadDictionaries = () => {
if (!fakeRendererWorker) {
fakeRendererWorker = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
nativeWindowOpen: true,
nodeIntegration: true
}
});
ipcMain.handle(APP_EVENTS.dictionaryGetMisspelled, handleGetMisspelled);
if (fakeRendererWorker) {
fakeRendererWorker.destroy();
}
fakeRendererWorker = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
nativeWindowOpen: true,
nodeIntegration: true
}
});
fakeRendererWorker.loadURL(`file://${__dirname}/dictionary.renderer/index.html`);
ipcMain.removeHandler(APP_EVENTS.dictionaryGetMisspelled);
ipcMain.handle(APP_EVENTS.dictionaryGetMisspelled, handleGetMisspelled);
// Uncomment to debug problems with dictionaries
// fakeRendererWorker.webContents.openDevTools();
};

const menuItem = ({webContents, suggestion}) => new MenuItem({
label: suggestion,
click: () => {
webContents.replaceMisspelling(suggestion);
}
});

const contextMenuHandler = async (_event, {misspelledWord}, webContents) => {
const {MenuItem} = require('electron');
const ret = [];
if (misspelledWord && misspelledWord.length > 0) {
const suggestions = await fakeRendererWorker.webContents.executeJavaScript(`getSuggestions('${misspelledWord}')`);
suggestions.forEach(suggestion =>
ret.push(new MenuItem({
label: suggestion,
click: () => {
webContents.replaceMisspelling(suggestion);
}
}))
);
suggestions.forEach(suggestion => ret.push(menuItem({webContents, suggestion})));
}
return ret;
};

const contextMenuNativeHandler = (_event, {misspelledWord, dictionarySuggestions = []}, webContents) => {
const ret = [];
if (misspelledWord && misspelledWord.length > 0) {
dictionarySuggestions.forEach(suggestion => ret.push(menuItem({webContents, suggestion})));
}
return ret;
};

module.exports = {
AVAILABLE_DICTIONARIES,
contextMenuHandler,
contextMenuNativeHandler,
getAvailableDictionaries,
getAvailableNativeDictionaries,
getEnabledDictionaries,
getUseNativeSpellChecker,
loadDictionaries
};
56 changes: 43 additions & 13 deletions src/tab-manager/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Tab Manager module test suite', () => {
mockBrowserView = {
setAutoResize: jest.fn(),
webContents: {
session: {},
executeJavaScript: jest.fn(async () => {}),
on: jest.fn(),
loadURL: jest.fn(),
Expand Down Expand Up @@ -198,7 +199,7 @@ describe('Tab Manager module test suite', () => {
mockMenu.append = jest.fn();
mockMenu.popup = jest.fn();
});
test('No spelling suggestions, should open a Menu with DevTools entry', async () => {
test('should open a Menu with DevTools entry', async () => {
// Given
spellChecker.contextMenuHandler.mockImplementationOnce(() => []);
// When
Expand All @@ -208,26 +209,55 @@ describe('Tab Manager module test suite', () => {
expect(electron.MenuItem).toHaveBeenCalledTimes(1);
expect(electron.MenuItem).toHaveBeenCalledWith(expect.objectContaining({label: 'DevTools'}));
expect(mockMenu.append).toHaveBeenCalledTimes(1);
expect(mockMenu.popup).toHaveBeenCalledTimes(1);
expect(mockMenu.popup).toHaveBeenCalledWith({x: 13, y: 37});
});
test('Spelling suggestions, should open a Menu with all suggestions, a sperator and DevTools entry', async () => {
test('should open a Menu at the specified location', async () => {
// Given
spellChecker.contextMenuHandler.mockImplementationOnce(() => [
new electron.MenuItem({label: 'suggestion 1'}),
new electron.MenuItem({label: 'suggestion 2'})
]);
spellChecker.contextMenuHandler.mockImplementationOnce(() => []);
// When
await events['context-menu'](new Event(''), {x: 13, y: 37});
// Then
expect(electron.Menu).toHaveBeenCalledTimes(1);
expect(electron.MenuItem).toHaveBeenCalledTimes(4);
expect(electron.MenuItem).toHaveBeenCalledWith({type: 'separator'});
expect(electron.MenuItem).toHaveBeenCalledWith(expect.objectContaining({label: 'DevTools'}));
expect(mockMenu.append).toHaveBeenCalledTimes(4);
expect(mockMenu.popup).toHaveBeenCalledTimes(1);
expect(mockMenu.popup).toHaveBeenCalledWith({x: 13, y: 37});
});
describe('with native spellcheck disabled', () => {
beforeEach(() => {
mockBrowserView.webContents.session.spellcheck = false;
});
test('Spelling suggestions, should open a Menu with all suggestions, a separator and DevTools entry', async () => {
// Given
spellChecker.contextMenuHandler.mockImplementationOnce(() => [
new electron.MenuItem({label: 'suggestion 1'}),
new electron.MenuItem({label: 'suggestion 2'})
]);
// When
await events['context-menu'](new Event(''), {x: 13, y: 37});
// Then
expect(electron.Menu).toHaveBeenCalledTimes(1);
expect(electron.MenuItem).toHaveBeenCalledTimes(4);
expect(electron.MenuItem).toHaveBeenCalledWith({type: 'separator'});
expect(electron.MenuItem).toHaveBeenCalledWith(expect.objectContaining({label: 'DevTools'}));
expect(mockMenu.append).toHaveBeenCalledTimes(4);
});
});
describe('with native spellcheck enabled', () => {
beforeEach(() => {
mockBrowserView.webContents.session.spellcheck = true;
});
test('Spelling suggestions, should open a Menu with all suggestions, a separator and DevTools entry', async () => {
// Given
spellChecker.contextMenuNativeHandler.mockImplementationOnce(() => [
new electron.MenuItem({label: 'suggestion 1'})
]);
// When
await events['context-menu'](new Event(''), {x: 13, y: 37});
// Then
expect(electron.Menu).toHaveBeenCalledTimes(1);
expect(electron.MenuItem).toHaveBeenCalledTimes(3);
expect(electron.MenuItem).toHaveBeenCalledWith({type: 'separator'});
expect(electron.MenuItem).toHaveBeenCalledWith(expect.objectContaining({label: 'DevTools'}));
expect(mockMenu.append).toHaveBeenCalledTimes(3);
});
});
});
});
});
Expand Down
Loading

0 comments on commit 9794439

Please sign in to comment.