Merge pull request #10938 from overleaf/em-esm-spelling

Migrate spelling to ES modules

GitOrigin-RevId: 4a200c8d1c28be44027cc8a3097e42575ab6593f
This commit is contained in:
Eric Mc Sween 2022-12-20 07:11:25 -05:00 committed by Copybot
parent 77c0802035
commit f6c1e2738d
21 changed files with 263 additions and 312 deletions

4
package-lock.json generated
View file

@ -37903,8 +37903,8 @@
"devDependencies": {
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"esmock": "^2.1.0",
"mocha": "^8.4.0",
"sandboxed-module": "2.0.4",
"sinon": "^9.2.4"
}
},
@ -48268,11 +48268,11 @@
"bunyan": "^1.8.15",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"esmock": "^2.1.0",
"express": "^4.17.1",
"lru-cache": "^5.1.1",
"mocha": "^8.4.0",
"request": "^2.88.2",
"sandboxed-module": "2.0.4",
"sinon": "^9.2.4",
"underscore": "1.13.1"
},

View file

@ -1,51 +1,22 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const metrics = require('@overleaf/metrics')
metrics.initialize('spelling')
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import { app } from './app/js/server.js'
import * as ASpell from './app/js/ASpell.js'
const Settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
logger.initialize('spelling')
if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
logger.initializeErrorReporting(Settings.sentry.dsn)
}
metrics.memory.monitor(logger)
const { host = 'localhost', port = 3005 } = Settings.internal?.spelling ?? {}
const SpellingAPIController = require('./app/js/SpellingAPIController')
const express = require('express')
const app = express()
metrics.injectMetricsRoute(app)
const bodyParser = require('body-parser')
const HealthCheckController = require('./app/js/HealthCheckController')
ASpell.startCacheDump()
app.use(bodyParser.json({ limit: '2mb' }))
app.use(metrics.http.monitor(logger))
const server = app.listen(port, host, function (error) {
if (error) {
throw error
}
logger.info({ host, port }, 'spelling HTTP server starting up')
})
app.post('/user/:user_id/check', SpellingAPIController.check)
app.get('/status', (req, res) => res.send({ status: 'spelling api is up' }))
app.get('/health_check', HealthCheckController.healthCheck)
const settings =
Settings.internal && Settings.internal.spelling
? Settings.internal.spelling
: undefined
const host = settings && settings.host ? settings.host : 'localhost'
const port = settings && settings.port ? settings.port : 3005
if (!module.parent) {
// application entry point, called directly
app.listen(port, host, function (error) {
if (error != null) {
throw error
}
return logger.debug(`spelling starting up, listening on ${host}:${port}`)
process.on('SIGTERM', () => {
ASpell.stopCacheDump()
server.close(() => {
logger.info({ host, port }, 'spelling HTTP server closed')
})
}
module.exports = app
})

View file

@ -7,14 +7,16 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const ASpellWorkerPool = require('./ASpellWorkerPool')
const LRU = require('lru-cache')
const logger = require('@overleaf/logger')
const fs = require('fs')
const settings = require('@overleaf/settings')
const Path = require('path')
const { promisify } = require('util')
const OError = require('@overleaf/o-error')
import fs from 'node:fs'
import Path from 'node:path'
import { promisify } from 'node:util'
import LRU from 'lru-cache'
import logger from '@overleaf/logger'
import settings from '@overleaf/settings'
import OError from '@overleaf/o-error'
import { ASpellWorkerPool } from './ASpellWorkerPool.js'
let ASPELL_TIMEOUT = 10000
const OneMinute = 60 * 1000
const opts = { max: 10000, maxAge: OneMinute * 60 * 10 }
@ -23,6 +25,8 @@ const cache = new LRU(opts)
const cacheFsPath = Path.resolve(settings.cacheDir, 'spell.cache')
const cacheFsPathTmp = cacheFsPath + '.tmp'
const WorkerPool = new ASpellWorkerPool()
// load any existing cache
try {
const oldCache = fs.readFileSync(cacheFsPath)
@ -33,24 +37,31 @@ try {
)
}
// write the cache every 30 minutes
const cacheDump = setInterval(function () {
const dump = JSON.stringify(cache.dump())
return fs.writeFile(cacheFsPathTmp, dump, function (err) {
if (err != null) {
logger.debug(OError.tag(err, 'error writing cache file'))
fs.unlink(cacheFsPathTmp, () => {})
} else {
fs.rename(cacheFsPathTmp, cacheFsPath, err => {
if (err) {
logger.error(OError.tag(err, 'error renaming cache file'))
} else {
logger.debug({ len: dump.length, cacheFsPath }, 'wrote cache file')
}
})
}
})
}, 30 * OneMinute)
let cacheDumpInterval
export function startCacheDump() {
// write the cache every 30 minutes
cacheDumpInterval = setInterval(function () {
const dump = JSON.stringify(cache.dump())
return fs.writeFile(cacheFsPathTmp, dump, function (err) {
if (err != null) {
logger.debug(OError.tag(err, 'error writing cache file'))
fs.unlink(cacheFsPathTmp, () => {})
} else {
fs.rename(cacheFsPathTmp, cacheFsPath, err => {
if (err) {
logger.error(OError.tag(err, 'error renaming cache file'))
} else {
logger.debug({ len: dump.length, cacheFsPath }, 'wrote cache file')
}
})
}
})
}, 30 * OneMinute)
}
export function stopCacheDump() {
clearInterval(cacheDumpInterval)
}
class ASpellRunner {
checkWords(language, words, callback) {
@ -159,33 +170,28 @@ class ASpellRunner {
words = Object.keys(newWord)
if (words.length) {
return WorkerPool.check(language, words, ASpell.ASPELL_TIMEOUT, callback)
return WorkerPool.check(language, words, ASPELL_TIMEOUT, callback)
} else {
return callback(null, '')
}
}
}
const ASpell = {
// The description of how to call aspell from another program can be found here:
// http://aspell.net/man-html/Through-A-Pipe.html
checkWords(language, words, callback) {
if (callback == null) {
callback = () => {}
}
const runner = new ASpellRunner()
return runner.checkWords(language, words, callback)
},
ASPELL_TIMEOUT: 10000,
// The description of how to call aspell from another program can be found here:
// http://aspell.net/man-html/Through-A-Pipe.html
export function checkWords(language, words, callback) {
if (callback == null) {
callback = () => {}
}
const runner = new ASpellRunner()
return runner.checkWords(language, words, callback)
}
const promises = {
checkWords: promisify(ASpell.checkWords),
export const promises = {
checkWords: promisify(checkWords),
}
ASpell.promises = promises
module.exports = ASpell
const WorkerPool = new ASpellWorkerPool()
module.exports.cacheDump = cacheDump
// for tests
export function setTimeout(timeout) {
ASPELL_TIMEOUT = timeout
}

View file

@ -7,15 +7,15 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const childProcess = require('child_process')
const logger = require('@overleaf/logger')
const metrics = require('@overleaf/metrics')
const _ = require('underscore')
const OError = require('@overleaf/o-error')
import childProcess from 'node:child_process'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import _ from 'underscore'
import OError from '@overleaf/o-error'
const BATCH_SIZE = 100
class ASpellWorker {
export class ASpellWorker {
constructor(language) {
this.language = language
this.count = 0
@ -238,5 +238,3 @@ class ASpellWorker {
return this.pipe.stdin.write(command + '\n')
}
}
module.exports = ASpellWorker

View file

@ -7,13 +7,13 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const ASpellWorker = require('./ASpellWorker')
const _ = require('underscore')
const logger = require('@overleaf/logger')
const metrics = require('@overleaf/metrics')
const OError = require('@overleaf/o-error')
import _ from 'underscore'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import OError from '@overleaf/o-error'
import { ASpellWorker } from './ASpellWorker.js'
class ASpellWorkerPool {
export class ASpellWorkerPool {
static initClass() {
this.prototype.MAX_REQUESTS = 100 * 1024
this.prototype.MAX_WORKERS = 32
@ -112,5 +112,3 @@ class ASpellWorkerPool {
}
}
ASpellWorkerPool.initClass()
module.exports = ASpellWorkerPool

View file

@ -1,39 +1,37 @@
const request = require('request')
const logger = require('@overleaf/logger')
const settings = require('@overleaf/settings')
const OError = require('@overleaf/o-error')
import request from 'request'
import logger from '@overleaf/logger'
import settings from '@overleaf/settings'
import OError from '@overleaf/o-error'
module.exports = {
healthCheck(req, res) {
const opts = {
url: `http://localhost:3005/user/${settings.healthCheckUserId}/check`,
json: {
words: ['helllo'],
language: 'en',
},
timeout: 1000 * 20,
export function healthCheck(req, res) {
const opts = {
url: `http://localhost:3005/user/${settings.healthCheckUserId}/check`,
json: {
words: ['helllo'],
language: 'en',
},
timeout: 1000 * 20,
}
return request.post(opts, function (err, response, body) {
if (err != null) {
return res.sendStatus(500)
}
return request.post(opts, function (err, response, body) {
if (err != null) {
return res.sendStatus(500)
}
const misspellings =
body && body.misspellings ? body.misspellings[0] : undefined
const numberOfSuggestions =
misspellings && misspellings.suggestions
? misspellings.suggestions.length
: 0
const misspellings =
body && body.misspellings ? body.misspellings[0] : undefined
const numberOfSuggestions =
misspellings && misspellings.suggestions
? misspellings.suggestions.length
: 0
if (numberOfSuggestions > 10) {
logger.debug('health check passed')
res.sendStatus(200)
} else {
logger.err(
new OError('health check failed', { body, numberOfSuggestions })
)
res.sendStatus(500)
}
})
},
if (numberOfSuggestions > 10) {
logger.debug('health check passed')
res.sendStatus(200)
} else {
logger.err(
new OError('health check failed', { body, numberOfSuggestions })
)
res.sendStatus(500)
}
})
}

View file

@ -1,31 +1,28 @@
const SpellingAPIManager = require('./SpellingAPIManager')
const logger = require('@overleaf/logger')
const metrics = require('@overleaf/metrics')
const OError = require('@overleaf/o-error')
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import OError from '@overleaf/o-error'
import * as SpellingAPIManager from './SpellingAPIManager.js'
function extractCheckRequestData(req) {
const token = req.params ? req.params.user_id : undefined
const wordCount =
req.body && req.body.words ? req.body.words.length : undefined
const token = req.params?.user_id
const wordCount = req.body?.words?.length
return { token, wordCount }
}
module.exports = {
check(req, res) {
metrics.inc('spelling-check', 0.1)
const { token, wordCount } = extractCheckRequestData(req)
logger.debug({ token, wordCount }, 'running check')
SpellingAPIManager.runRequest(token, req.body, function (error, result) {
if (error != null) {
logger.error(
OError.tag(error, 'error processing spelling request', {
user_id: token,
wordCount,
})
)
return res.sendStatus(500)
}
res.send(result)
})
},
export function check(req, res) {
metrics.inc('spelling-check', 0.1)
const { token, wordCount } = extractCheckRequestData(req)
logger.debug({ token, wordCount }, 'running check')
SpellingAPIManager.runRequest(token, req.body, (error, result) => {
if (error != null) {
logger.error(
OError.tag(error, 'error processing spelling request', {
user_id: token,
wordCount,
})
)
return res.sendStatus(500)
}
res.send(result)
})
}

View file

@ -6,31 +6,26 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const ASpell = require('./ASpell')
const { callbackify } = require('util')
const OError = require('@overleaf/o-error')
import { callbackify } from 'node:util'
import OError from '@overleaf/o-error'
import * as ASpell from './ASpell.js'
// The max number of words checked in a single request
const REQUEST_LIMIT = 10000
const SpellingAPIManager = {}
export const promises = {}
const promises = {
async runRequest(token, request) {
if (!request.words) {
throw new OError('malformed JSON')
}
const lang = request.language || 'en'
promises.runRequest = async (token, request) => {
if (!request.words) {
throw new OError('malformed JSON')
}
const lang = request.language || 'en'
// only the first 10K words are checked
const wordSlice = request.words.slice(0, REQUEST_LIMIT)
// only the first 10K words are checked
const wordSlice = request.words.slice(0, REQUEST_LIMIT)
const misspellings = await ASpell.promises.checkWords(lang, wordSlice)
return { misspellings }
},
const misspellings = await ASpell.promises.checkWords(lang, wordSlice)
return { misspellings }
}
SpellingAPIManager.runRequest = callbackify(promises.runRequest)
SpellingAPIManager.promises = promises
module.exports = SpellingAPIManager
export const runRequest = callbackify(promises.runRequest)

View file

@ -0,0 +1,26 @@
import metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import express from 'express'
import bodyParser from 'body-parser'
import * as SpellingAPIController from './SpellingAPIController.js'
import * as HealthCheckController from './HealthCheckController.js'
metrics.initialize('spelling')
logger.initialize('spelling')
if (Settings.sentry?.dsn != null) {
logger.initializeErrorReporting(Settings.sentry.dsn)
}
metrics.memory.monitor(logger)
export const app = express()
metrics.injectMetricsRoute(app)
app.use(bodyParser.json({ limit: '2mb' }))
app.use(metrics.http.monitor(logger))
app.post('/user/:user_id/check', SpellingAPIController.check)
app.get('/status', (req, res) => res.send({ status: 'spelling api is up' }))
app.get('/health_check', HealthCheckController.healthCheck)

View file

@ -3,12 +3,13 @@
"description": "A JSON API wrapper around aspell",
"private": true,
"main": "app.js",
"type": "module",
"scripts": {
"compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')",
"start": "node $NODE_APP_OPTIONS app.js",
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
"test:acceptance:_run": "LOG_LEVEL=fatal mocha --loader=esmock --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
"test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
"test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
"test:unit:_run": "LOG_LEVEL=fatal mocha --loader=esmock --recursive --reporter spec $@ test/unit/js",
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
"compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee",
"compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee",
@ -37,8 +38,8 @@
"devDependencies": {
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"esmock": "^2.1.0",
"mocha": "^8.4.0",
"sandboxed-module": "2.0.4",
"sinon": "^9.2.4"
}
}

View file

@ -1,5 +1,5 @@
const { expect } = require('chai')
const request = require('./helpers/request')
import { expect } from 'chai'
import * as request from './helpers/request.js'
const USER_ID = 101

View file

@ -1,5 +1,5 @@
const { expect } = require('chai')
const request = require('./helpers/request')
import { expect } from 'chai'
import * as request from './helpers/request.js'
describe('/health_check', function () {
it('should return 200', async function () {

View file

@ -1,5 +1,6 @@
const App = require('../../../app.js')
const { PORT } = require('./helpers/request')
import { app } from '../../../app/js/server.js'
import { PORT } from './helpers/request.js'
before(function (done) {
return App.listen(PORT, 'localhost', done)
return app.listen(PORT, 'localhost', done)
})

View file

@ -1,5 +1,5 @@
const { expect } = require('chai')
const request = require('./helpers/request')
import { expect } from 'chai'
import * as request from './helpers/request.js'
describe('/status', function () {
it('should return 200', async function () {

View file

@ -1,10 +1,11 @@
const { promisify } = require('util')
import { promisify } from 'util'
import Request from 'request'
const PORT = 3005
export const PORT = 3005
const BASE_URL = `http://${process.env.HTTP_TEST_HOST || 'localhost'}:${PORT}`
const request = require('request').defaults({
const request = Request.defaults({
baseUrl: BASE_URL,
headers: {
'Content-Type': 'application/json',
@ -12,9 +13,6 @@ const request = require('request').defaults({
followRedirect: false,
})
module.exports = {
PORT,
get: promisify(request.get),
post: promisify(request.post),
del: promisify(request.del),
}
export const get = promisify(request.get)
export const post = promisify(request.post)
export const del = promisify(request.del)

View file

@ -1,21 +1,4 @@
const chai = require('chai')
const SandboxedModule = require('sandboxed-module')
import chai from 'chai'
// Chai configuration
chai.should()
// SandboxedModule configuration
SandboxedModule.configure({
requires: {
'@overleaf/logger': {
debug() {},
log() {},
info() {},
warn() {},
err() {},
error() {},
fatal() {},
},
},
globals: { Buffer, JSON, console, process },
})

View file

@ -13,9 +13,9 @@
// send P correct words and Q incorrect words
// generate incorrect words by qq+random
const async = require('async')
const request = require('request')
const fs = require('fs')
import fs from 'node:fs'
import async from 'async'
import request from 'request'
// created with
// aspell -d en dump master | aspell -l en expand | shuf -n 150000 > words.txt

View file

@ -8,22 +8,17 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { expect, assert } = require('chai')
const SandboxedModule = require('sandboxed-module')
import { expect, assert } from 'chai'
import esmock from 'esmock'
describe('ASpell', function () {
beforeEach(function () {
return (this.ASpell = SandboxedModule.require('../../../app/js/ASpell', {
requires: {
'@overleaf/metrics': {
gauge() {},
inc() {},
},
beforeEach(async function () {
this.ASpell = await esmock('../../../app/js/ASpell', {
'@overleaf/metrics': {
gauge() {},
inc() {},
},
}))
})
afterEach(function () {
clearInterval(this.ASpell.cacheDump)
})
})
describe('a correctly spelled word', function () {
@ -114,7 +109,7 @@ describe('ASpell', function () {
return describe('when the request times out', function () {
beforeEach(function (done) {
const words = __range__(0, 1000, true).map(i => 'abcdefg')
this.ASpell.ASPELL_TIMEOUT = 1
this.ASpell.setTimeout(1)
this.start = Date.now()
return this.ASpell.checkWords('en', words, (error, result) => {
expect(error).to.exist
@ -128,7 +123,7 @@ describe('ASpell', function () {
// or the CI server.
return it('should return in reasonable time', function () {
const delta = Date.now() - this.start
return delta.should.be.below(this.ASpell.ASPELL_TIMEOUT + 1000)
return delta.should.be.below(1000)
})
})
})

View file

@ -1,45 +1,39 @@
/* eslint-disable
no-undef
*/
const sinon = require('sinon')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const EventEmitter = require('events')
import sinon from 'sinon'
import { expect } from 'chai'
import esmock from 'esmock'
import EventEmitter from 'events'
describe('ASpellWorker', function () {
beforeEach(function () {
this.child_process = {}
return (this.ASpellWorker = SandboxedModule.require(
'../../../app/js/ASpellWorker',
{
requires: {
'@overleaf/metrics': {
gauge() {},
inc() {},
},
child_process: this.child_process,
},
}
))
beforeEach(async function () {
this.pipe = {
stdout: new EventEmitter(),
stderr: { on: sinon.stub() },
stdin: { on: sinon.stub() },
on: sinon.stub(),
pid: 12345,
}
this.pipe.stdout.setEncoding = sinon.stub()
this.child_process = {
spawn: sinon.stub().returns(this.pipe),
}
const { ASpellWorker } = await esmock('../../../app/js/ASpellWorker', {
'@overleaf/metrics': {
gauge() {},
inc() {},
},
child_process: this.child_process,
})
this.ASpellWorker = ASpellWorker
})
describe('creating a worker', function () {
beforeEach(function () {
this.pipe = {
stdout: new EventEmitter(),
stderr: { on: sinon.stub() },
stdin: { on: sinon.stub() },
on: sinon.stub(),
pid: 12345,
}
this.child_process.spawn = sinon.stub().returns(this.pipe)
this.pipe.stdout.setEncoding = sinon.stub()
worker = new this.ASpellWorker('en')
this.worker = new this.ASpellWorker('en')
})
describe('with normal aspell output', function () {
beforeEach(function () {
this.callback = worker.callback = sinon.stub()
this.callback = this.worker.callback = sinon.stub()
this.pipe.stdout.emit('data', '& hello\n')
this.pipe.stdout.emit('data', '& world\n')
this.pipe.stdout.emit('data', 'en\n')
@ -56,7 +50,7 @@ describe('ASpellWorker', function () {
describe('with the aspell end marker split across chunks', function () {
beforeEach(function () {
this.callback = worker.callback = sinon.stub()
this.callback = this.worker.callback = sinon.stub()
this.pipe.stdout.emit('data', '& hello\n')
this.pipe.stdout.emit('data', '& world\ne')
this.pipe.stdout.emit('data', 'n\n')
@ -73,7 +67,7 @@ describe('ASpellWorker', function () {
describe('with the aspell end marker newline split across chunks', function () {
beforeEach(function () {
this.callback = worker.callback = sinon.stub()
this.callback = this.worker.callback = sinon.stub()
this.pipe.stdout.emit('data', '& hello\n')
this.pipe.stdout.emit('data', '& world\n')
this.pipe.stdout.emit('data', 'en')
@ -90,7 +84,7 @@ describe('ASpellWorker', function () {
describe('with everything split across chunks', function () {
beforeEach(function () {
this.callback = worker.callback = sinon.stub()
this.callback = this.worker.callback = sinon.stub()
'& hello\n& world\nen\n& goodbye'.split('').forEach(x => {
this.pipe.stdout.emit('data', x)
})

View file

@ -1,48 +1,38 @@
/* eslint-disable
handle-callback-err
*/
const sinon = require('sinon')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const modulePath = require('path').join(
__dirname,
'../../../app/js/SpellingAPIManager'
)
import sinon from 'sinon'
import { expect } from 'chai'
import esmock from 'esmock'
const MODULE_PATH = '../../../app/js/SpellingAPIManager'
const promiseStub = val => new Promise(resolve => resolve(val))
describe('SpellingAPIManager', function () {
beforeEach(function () {
beforeEach(async function () {
this.token = 'user-id-123'
this.ASpell = {}
this.SpellingAPIManager = SandboxedModule.require(modulePath, {
requires: {
'./ASpell': this.ASpell,
'@overleaf/settings': { ignoredMisspellings: ['ShareLaTeX'] },
this.nonLearnedWords = ['some', 'words', 'htat', 'are', 'speled', 'rong']
this.allWords = this.nonLearnedWords
this.misspellings = [
{ index: 2, suggestions: ['that'] },
{ index: 4, suggestions: ['spelled'] },
{ index: 5, suggestions: ['wrong', 'ring'] },
]
this.misspellingsWithoutLearnedWords = this.misspellings.slice(0, 3)
this.ASpell = {
checkWords: sinon.stub().yields(null, this.misspellings),
promises: {
checkWords: sinon.stub().returns(promiseStub(this.misspellings)),
},
}
this.SpellingAPIManager = await esmock(MODULE_PATH, {
'../../../app/js/ASpell.js': this.ASpell,
'@overleaf/settings': { ignoredMisspellings: ['ShareLaTeX'] },
})
})
describe('runRequest', function () {
beforeEach(function () {
this.nonLearnedWords = ['some', 'words', 'htat', 'are', 'speled', 'rong']
this.allWords = this.nonLearnedWords
this.misspellings = [
{ index: 2, suggestions: ['that'] },
{ index: 4, suggestions: ['spelled'] },
{ index: 5, suggestions: ['wrong', 'ring'] },
]
this.misspellingsWithoutLearnedWords = this.misspellings.slice(0, 3)
this.ASpell.checkWords = (lang, word, callback) => {
callback(null, this.misspellings)
}
this.ASpell.promises = {
checkWords: sinon.stub().returns(promiseStub(this.misspellings)),
}
sinon.spy(this.ASpell, 'checkWords')
})
describe('with sensible JSON', function () {
beforeEach(function (done) {
this.SpellingAPIManager.runRequest(