Use Hunspell for client-side spellchecking (#20286)

GitOrigin-RevId: c4d0d9e06fe0cc9d7cb7a058fd0768eb024e44f5
This commit is contained in:
Alf Eaton
2024-10-14 10:58:36 +00:00
committed by Copybot
parent 2d737810d4
commit 24c8629cd4
40 changed files with 5391 additions and 514 deletions
@@ -0,0 +1,100 @@
import DictionaryModal from '@/features/dictionary/components/dictionary-modal'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('<DictionaryModalContent />', function () {
beforeEach(function () {
cy.interceptCompile()
})
afterEach(function () {
cy.window().then(win => {
win.dispatchEvent(new CustomEvent('learnedWords:doreset'))
})
})
it('list words', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-learnedWords', ['foo', 'bar'])
win.dispatchEvent(new CustomEvent('learnedWords:doreset'))
})
cy.mount(
<EditorProviders>
<DictionaryModal show handleHide={cy.stub()} />
</EditorProviders>
)
cy.findByText('foo')
cy.findByText('bar')
})
it('shows message when empty', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-learnedWords', [])
win.dispatchEvent(new CustomEvent('learnedWords:doreset'))
})
cy.mount(
<EditorProviders>
<DictionaryModal show handleHide={cy.stub()} />
</EditorProviders>
)
cy.contains('Your custom dictionary is empty.')
})
it('removes words', function () {
cy.intercept('/spelling/unlearn', { statusCode: 200 })
cy.window().then(win => {
win.metaAttributesCache.set('ol-learnedWords', ['Foo', 'bar'])
win.dispatchEvent(new CustomEvent('learnedWords:doreset'))
})
cy.mount(
<EditorProviders>
<DictionaryModal show handleHide={cy.stub()} />
</EditorProviders>
)
cy.findByText('Foo')
cy.findByText('bar')
cy.findAllByRole('button', {
name: 'Remove from dictionary',
})
.eq(0)
.click()
cy.findByText('bar').should('not.exist')
cy.findByText('Foo')
})
it('handles errors', function () {
cy.intercept('/spelling/unlearn', { statusCode: 500 }).as('unlearn')
cy.window().then(win => {
win.metaAttributesCache.set('ol-learnedWords', ['foo'])
win.dispatchEvent(new CustomEvent('learnedWords:doreset'))
})
cy.mount(
<EditorProviders>
<DictionaryModal show handleHide={cy.stub()} />
</EditorProviders>
)
cy.findByText('foo')
cy.findAllByRole('button', {
name: 'Remove from dictionary',
})
.eq(0)
.click()
cy.wait('@unlearn')
cy.findByText('Sorry, something went wrong')
cy.findByText('foo')
})
})
@@ -1,60 +0,0 @@
import {
screen,
fireEvent,
waitForElementToBeRemoved,
} from '@testing-library/react'
import fetchMock from 'fetch-mock'
import DictionaryModal from '../../../../../frontend/js/features/dictionary/components/dictionary-modal'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
function setLearnedWords(words) {
window.metaAttributesCache.set('ol-learnedWords', words)
window.dispatchEvent(new CustomEvent('learnedWords:doreset'))
}
describe('<DictionaryModalContent />', function () {
afterEach(function () {
fetchMock.reset()
setLearnedWords([])
})
it('list words', async function () {
setLearnedWords(['foo', 'bar'])
renderWithEditorContext(<DictionaryModal show handleHide={() => {}} />)
screen.getByText('foo')
screen.getByText('bar')
})
it('shows message when empty', async function () {
setLearnedWords([])
renderWithEditorContext(<DictionaryModal show handleHide={() => {}} />)
screen.getByText('Your custom dictionary is empty.')
})
it('removes words', async function () {
fetchMock.post('/spelling/unlearn', 200)
setLearnedWords(['Foo', 'bar'])
renderWithEditorContext(<DictionaryModal show handleHide={() => {}} />)
screen.getByText('Foo')
screen.getByText('bar')
const [firstButton] = screen.getAllByRole('button', {
name: 'Remove from dictionary',
})
fireEvent.click(firstButton)
await waitForElementToBeRemoved(() => screen.getByText('bar'))
screen.getByText('Foo')
})
it('handles errors', async function () {
fetchMock.post('/spelling/unlearn', 500)
setLearnedWords(['foo'])
renderWithEditorContext(<DictionaryModal show handleHide={() => {}} />)
screen.getByText('foo')
const [firstButton] = screen.getAllByRole('button', {
name: 'Remove from dictionary',
})
fireEvent.click(firstButton)
await fetchMock.flush()
screen.getByText('Sorry, something went wrong')
screen.getByText('foo')
})
})
@@ -0,0 +1,85 @@
import '../../../helpers/bootstrap-3'
import { mockScope } from '../helpers/mock-scope'
import { EditorProviders } from '../../../helpers/editor-providers'
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
import { TestContainer } from '../helpers/test-container'
import forEach from 'mocha-each'
import PackageVersions from '../../../../../app/src/infrastructure/PackageVersions'
const languages = [
{ code: 'en_GB', dic: 'en_GB', name: 'English (British)' },
{ code: 'fr', dic: 'fr', name: 'French' },
{ code: 'sv', dic: 'sv_SE', name: 'Swedish' },
]
const suggestions = {
en_GB: ['medecine', 'medicine'],
fr: ['medecin', 'médecin'],
sv: ['medecin', 'medicin'],
}
forEach(Object.keys(suggestions)).describe(
'Spell check in client (%s)',
(spellCheckLanguage: keyof typeof suggestions) => {
const content = `
\\documentclass{}
\\title{}
\\author{}
\\begin{document}
\\maketitle
\\begin{abstract}
\\end{abstract}
\\section{}
\\end{document}`
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-preventCompileOnLoad', true)
win.metaAttributesCache.set('ol-splitTestVariants', {
'spell-check-client': 'enabled',
})
win.metaAttributesCache.set('ol-splitTestInfo', {})
win.metaAttributesCache.set('ol-learnedWords', ['baz'])
win.metaAttributesCache.set(
'ol-dictionariesRoot',
`js/dictionaries/${PackageVersions.version.dictionaries}/`
)
win.metaAttributesCache.set('ol-baseAssetPath', '/__cypress/src/')
win.metaAttributesCache.set('ol-languages', languages)
})
cy.interceptEvents()
const scope = mockScope(content)
scope.project.spellCheckLanguage = spellCheckLanguage
cy.mount(
<TestContainer>
<EditorProviders scope={scope}>
<CodeMirrorEditor />
</EditorProviders>
</TestContainer>
)
cy.get('.cm-line').eq(13).as('line')
cy.get('@line').click()
})
it('shows suggestions for misspelled word', function () {
const [from, to] = suggestions[spellCheckLanguage]
cy.get('@line').type(from)
cy.get('@line').get('.ol-cm-spelling-error').contains(from)
cy.get('@line').get('.ol-cm-spelling-error').rightclick()
cy.findByText(to).click()
cy.get('@line').contains(to)
cy.get('@line').find('.ol-cm-spelling-error').should('not.exist')
})
}
)