first commit
This commit is contained in:
@@ -0,0 +1,990 @@
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { docId, mockDocContent } from '../helpers/mock-doc'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { activeEditorLine } from '../helpers/active-editor-line'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { FC } from 'react'
|
||||
import { MetadataContext } from '@/features/ide-react/context/metadata-context'
|
||||
import { ReferencesContext } from '@/features/ide-react/context/references-context'
|
||||
|
||||
describe('autocomplete', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
window.metaAttributesCache.set('ol-showSymbolPalette', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
})
|
||||
|
||||
it('opens autocomplete on matched text', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
_id: 'test-folder-id',
|
||||
name: 'test-folder',
|
||||
docs: [
|
||||
{
|
||||
_id: 'test-doc-in-folder',
|
||||
name: 'example.tex',
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: 'test-file-in-folder',
|
||||
name: 'example.png',
|
||||
hash: '42',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: 'test-image-file',
|
||||
name: 'frog.jpg',
|
||||
hash: '21',
|
||||
},
|
||||
{
|
||||
_id: 'uppercase-extension-image-file',
|
||||
name: 'frog.JPG',
|
||||
hash: '22',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
cy.contains('\\section{Results}')
|
||||
|
||||
// no autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 0)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// single backslash
|
||||
cy.get('@line').type('\\')
|
||||
|
||||
// autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
// another backslash
|
||||
cy.get('@line').type('\\')
|
||||
|
||||
// no autocomplete open
|
||||
cy.findAllByRole('listbox').should('have.length', 0)
|
||||
|
||||
// a space then another backslash
|
||||
cy.get('@line').type(' \\')
|
||||
|
||||
// autocomplete open
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
// start a command
|
||||
cy.get('@line').type('includegr')
|
||||
|
||||
// select option from autocomplete
|
||||
// disabled as selector not working (Cypress bug?)
|
||||
// cy.findByRole('listbox')
|
||||
// .findByRole('option', {
|
||||
// name: '\\includegraphics[]{}',
|
||||
// selected: true,
|
||||
// })
|
||||
// .click()
|
||||
cy.contains('\\includegraphics[]{}').click()
|
||||
|
||||
// start a command in the optional argument
|
||||
cy.get('@line').type('width=0.3\\text')
|
||||
|
||||
// select option from autocomplete
|
||||
// disabled as selector not working (Cypress bug?)
|
||||
// cy.findByRole('listbox')
|
||||
// .findByRole('option', {
|
||||
// name: '\\textwidth',
|
||||
// selected: false,
|
||||
// })
|
||||
// .click()
|
||||
cy.contains('\\textwidth').click()
|
||||
|
||||
// move to required argument and start a label
|
||||
cy.get('@line')
|
||||
// .type('{tab}') // Tab not supported in Cypress
|
||||
.type('{rightArrow}{rightArrow}')
|
||||
cy.get('@line').type('fr')
|
||||
|
||||
// select option from autocomplete
|
||||
// disabled as selector not working (Cypress bug?)
|
||||
// cy.findByRole('listbox')
|
||||
// .findByRole('option', {
|
||||
// name: 'frog.jpg',
|
||||
// selected: true,
|
||||
// })
|
||||
// .click()
|
||||
cy.contains('frog.jpg').click()
|
||||
|
||||
cy.contains('\\includegraphics[width=0.3\\textwidth]{frog.jpg}')
|
||||
|
||||
// start a new line and select an "includegraphics" command completion
|
||||
cy.get('@line').type('{Enter}')
|
||||
activeEditorLine().type('\\includegr')
|
||||
cy.contains('\\includegraphics[]{}').click()
|
||||
|
||||
// select a completion for a file in a folder, without typing the folder name
|
||||
activeEditorLine()
|
||||
.type('{rightArrow}{rightArrow}')
|
||||
.type('examp')
|
||||
.type('{Backspace}')
|
||||
.type('ple')
|
||||
cy.contains('test-folder/example.png').click()
|
||||
cy.contains('\\includegraphics[]{test-folder/example.png}')
|
||||
|
||||
activeEditorLine()
|
||||
.type(`${'{leftArrow}'.repeat('test-folder/example.png}'.length)}fr`)
|
||||
.type('{ctrl+ }')
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
cy.findByRole('listbox').contains('frog.JPG').click()
|
||||
activeEditorLine().should('have.text', '\\includegraphics[]{frog.JPG}')
|
||||
})
|
||||
|
||||
it('opens autocomplete on begin environment', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
_id: 'test-folder-id',
|
||||
name: 'test-folder',
|
||||
docs: [
|
||||
{
|
||||
_id: 'test-doc-in-folder',
|
||||
name: 'example.tex',
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: 'test-file-in-folder',
|
||||
name: 'example.png',
|
||||
hash: '42',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: 'test-image-file',
|
||||
name: 'frog.jpg',
|
||||
hash: '43',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
cy.contains('\\section{Results}')
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// ---- Basic autocomplete of environments
|
||||
cy.get('@line').type('\\begin{itemi')
|
||||
cy.findAllByRole('option').contains('\\begin{itemize}').click()
|
||||
cy.get('.cm-line').eq(16).contains('\\begin{itemize}')
|
||||
cy.get('.cm-line').eq(17).contains('\\item')
|
||||
cy.get('.cm-line').eq(18).contains('\\end{itemize}')
|
||||
|
||||
// ---- Autocomplete on a malformed `\begin{`
|
||||
// first, ensure that the "abcdef" environment is present in the doc
|
||||
cy.get('.cm-line')
|
||||
.eq(20)
|
||||
.type('\\begin{{}abcdef}{Enter}{Enter}\\end{{}abcdef}{Enter}{Enter}')
|
||||
|
||||
cy.get('.cm-line').eq(24).as('line')
|
||||
|
||||
cy.get('@line').type('\\begin{abcdef')
|
||||
cy.findAllByRole('option').contains('\\begin{abcdef}').click()
|
||||
|
||||
cy.get('.cm-line').eq(24).contains('\\begin{abcdef}')
|
||||
cy.get('.cm-line').eq(26).contains('\\end{abcdef}')
|
||||
|
||||
// ---- Autocomplete starting from end of `\begin`
|
||||
cy.get('.cm-line').eq(22).type('{Enter}{Enter}{Enter}')
|
||||
|
||||
cy.get('.cm-line').eq(24).as('line')
|
||||
cy.get('@line').type('\\begin {leftArrow}{leftArrow}')
|
||||
cy.get('@line').type('{ctrl} ')
|
||||
|
||||
cy.findAllByRole('option').contains('\\begin{align}').click()
|
||||
|
||||
cy.get('.cm-line').eq(24).contains('\\begin{align}')
|
||||
cy.get('.cm-line').eq(26).contains('\\end{align}')
|
||||
|
||||
// ---- Start typing a begin command
|
||||
cy.get('.cm-line').eq(28).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('\\begin{{}ab')
|
||||
cy.findAllByRole('option').as('options')
|
||||
cy.get('@options').should('have.length', 5)
|
||||
|
||||
// ---- The environment being typed should appear in the list
|
||||
cy.get('@options').contains('\\begin{ab}')
|
||||
|
||||
// ---- A new environment used elsewhere in the doc should appear next
|
||||
cy.get('@options')
|
||||
.eq(0)
|
||||
.invoke('text')
|
||||
.should('match', /^\\begin\{abcdef}/)
|
||||
|
||||
// ---- The built-in environments should appear at the top of the list
|
||||
cy.get('@options')
|
||||
.eq(1)
|
||||
.invoke('text')
|
||||
.should('match', /^\\begin\{abstract}/)
|
||||
})
|
||||
|
||||
it('opens autocomplete using metadata for usepackage parameter', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
const metadata = {
|
||||
commands: [],
|
||||
labels: new Set<string>(),
|
||||
packageNames: new Set(['foo']),
|
||||
}
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider value={metadata}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
rootFolder={rootFolder as any}
|
||||
providers={{ MetadataProvider }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// no autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 0)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// a usepackage command
|
||||
cy.get('@line').type('\\usepackage')
|
||||
|
||||
// autocomplete open
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
cy.findAllByRole('option').eq(0).should('contain.text', '\\usepackage{}')
|
||||
cy.findAllByRole('option').eq(1).should('contain.text', '\\usepackage[]{}')
|
||||
|
||||
// the start of a package name from the metadata
|
||||
cy.get('@line').type('{fo')
|
||||
|
||||
// autocomplete open
|
||||
cy.findAllByRole('listbox')
|
||||
.should('have.length', 1)
|
||||
.type('{downArrow}{downArrow}{Enter}')
|
||||
|
||||
cy.contains('\\usepackage{foo}')
|
||||
})
|
||||
|
||||
it('opens autocomplete using metadata for cite parameter', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
const ReferencesProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<ReferencesContext.Provider
|
||||
value={{
|
||||
referenceKeys: new Set(['ref-1', 'ref-2', 'ref-3']),
|
||||
indexAllReferences: cy.stub(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReferencesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ReferencesProvider }}
|
||||
rootFolder={rootFolder as any}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// no autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 0)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// a cite command with no opening brace
|
||||
cy.get('@line').type('\\cite')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\cite{}').click()
|
||||
cy.get('@line').contains('\\cite{}')
|
||||
|
||||
// autocomplete open again
|
||||
cy.findAllByRole('listbox').contains('ref-2').click()
|
||||
cy.get('@line').contains('\\cite{ref-2}')
|
||||
|
||||
// start typing another reference
|
||||
cy.get('@line').type('{leftArrow}, re')
|
||||
|
||||
// autocomplete open again
|
||||
cy.findAllByRole('listbox').contains('ref-3').click()
|
||||
cy.get('@line').contains('\\cite{ref-2, ref-3}')
|
||||
})
|
||||
|
||||
it('autocomplete stops after space after command', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
cy.contains('\\section{Results}')
|
||||
|
||||
// no autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 0)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// single backslash
|
||||
cy.get('@line').type('\\')
|
||||
|
||||
// autocomplete
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
// start a command
|
||||
cy.get('@line').type('ite')
|
||||
|
||||
// offers completion for item
|
||||
cy.contains(/\\item.*cmd/).click()
|
||||
|
||||
cy.get('@line').type('{Enter}{Enter}\\item ')
|
||||
cy.contains('\\begin{itemize').should('not.exist')
|
||||
})
|
||||
|
||||
it('autocomplete does not remove closing brackets in commands with multiple braces {}{}', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('@line').type('\\frac')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\frac{}{}').click()
|
||||
|
||||
cy.get('@line').type('\\textbf')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\textbf{}').click()
|
||||
|
||||
cy.get('@line').should('contain.text', '\\frac{\\textbf{}}{}')
|
||||
|
||||
// go to new line
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('{enter}')
|
||||
|
||||
cy.get('.cm-line').eq(17).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('\\frac')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\frac{}{}').click()
|
||||
|
||||
cy.get('@line').type('\\partial')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\partial').click()
|
||||
|
||||
cy.get('@line').should('contain.text', '\\frac{\\partial}{}')
|
||||
})
|
||||
|
||||
it('autocomplete does not remove paired closing brackets in nested commands', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// type some commands
|
||||
// note: '{{}' is a single opening brace
|
||||
cy.get('@line').type('\\sqrt{{}2} some more text \\sqrt{{}\\sqrt{{}}}')
|
||||
|
||||
// put the cursor inside the middle pair of braces
|
||||
cy.get('@line').type('{leftArrow}{leftArrow}')
|
||||
|
||||
// start a command
|
||||
cy.get('@line').type('\\sqrt')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\sqrt{}').click()
|
||||
|
||||
// assert that the existing closing brace hasn't been removed
|
||||
cy.get('@line').should(
|
||||
'have.text',
|
||||
'\\sqrt{2} some more text \\sqrt{\\sqrt{\\sqrt{}}}'
|
||||
)
|
||||
})
|
||||
|
||||
it('autocomplete does remove unpaired closing brackets in nested commands', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// type some commands
|
||||
// note: '{{}' is a single opening brace
|
||||
cy.get('@line').type('\\sqrt{{}2} some more text \\sqrt{{}\\sqrt{{}}}}')
|
||||
|
||||
// put the cursor inside the middle pair of braces
|
||||
cy.get('@line').type('{leftArrow}{leftArrow}{leftArrow}')
|
||||
|
||||
// start a command
|
||||
cy.get('@line').type('\\sqrt')
|
||||
|
||||
// select completion
|
||||
cy.findAllByRole('listbox').contains('\\sqrt{}').click()
|
||||
|
||||
// assert that the existing closing brace hasn't been removed
|
||||
cy.get('@line').should(
|
||||
'have.text',
|
||||
'\\sqrt{2} some more text \\sqrt{\\sqrt{\\sqrt{}}}'
|
||||
)
|
||||
})
|
||||
|
||||
it('displays completions for existing commands with multiple parameters', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// a new command, then the start of the command on a new blank line
|
||||
cy.get('@line').type('\\foo[bar]{{}baz}{{}zap}')
|
||||
|
||||
// enter, to create a new line
|
||||
cy.get('@editor').trigger('keydown', { key: 'Enter' })
|
||||
|
||||
// put the cursor on the new line to type in
|
||||
cy.get('.cm-line').eq(17).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// the start of the command
|
||||
cy.get('@line').type('\\foo')
|
||||
|
||||
// select the new completion
|
||||
cy.findAllByRole('listbox').contains('\\foo[]{}{}').click()
|
||||
|
||||
// fill in the optional parameter
|
||||
cy.get('@line').type('bar')
|
||||
|
||||
cy.get('@editor').contains('\\foo[bar]{}{}')
|
||||
})
|
||||
|
||||
it('displays completions for existing commands in math mode', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// a new command, then the start of the command on a new blank line
|
||||
cy.get('@line').type('$\\somemathcommand$')
|
||||
|
||||
// enter, to create a new line
|
||||
cy.get('@editor').trigger('keydown', { key: 'Enter' })
|
||||
|
||||
// put the cursor on the new line to type in
|
||||
cy.get('.cm-line').eq(17).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// the start of the command
|
||||
cy.get('@line').type('hello \\somema')
|
||||
|
||||
// select the new completion
|
||||
cy.findAllByRole('listbox').contains('\\somemathcommand').click()
|
||||
|
||||
cy.get('@editor').contains('hello \\somemathcommand')
|
||||
})
|
||||
|
||||
it('displays completions for nested existing commands', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// a new command, then the start of the command on a new blank line
|
||||
cy.get('@line').type('\\newcommand{{}\\foo}[1]{{}#1}')
|
||||
|
||||
// enter, to create a new line
|
||||
cy.get('@editor').trigger('keydown', { key: 'Enter' })
|
||||
|
||||
// put the cursor on the new line to type in
|
||||
cy.get('.cm-line').eq(17).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// the start of the command
|
||||
cy.get('@line').type('\\fo')
|
||||
|
||||
// select the new completion
|
||||
cy.findAllByRole('listbox')
|
||||
.contains(/\\foo{}\s*cmd/)
|
||||
.click()
|
||||
|
||||
cy.get('@line').contains('\\foo')
|
||||
})
|
||||
|
||||
it('displays unique completions for commands', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
const metadata = {
|
||||
commands: [
|
||||
{
|
||||
caption: '\\label{}', // label{} is also included in top-hundred-snippets
|
||||
meta: 'amsmath-cmd',
|
||||
score: 1,
|
||||
snippet: '\\label{$1}',
|
||||
},
|
||||
],
|
||||
labels: new Set<string>(),
|
||||
packageNames: new Set<string>('amsmath'),
|
||||
}
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider value={metadata}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ MetadataProvider }}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// start typing a command
|
||||
cy.get('@line').type('\\label')
|
||||
|
||||
cy.findAllByRole('option').contains('\\label{}').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('displays symbol completions in autocomplete when the feature is enabled', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
window.metaAttributesCache.set('ol-showSymbolPalette', true)
|
||||
const user = {
|
||||
id: '123abd',
|
||||
email: 'testuser@example.com',
|
||||
}
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders user={user} scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// type the name of a symbol
|
||||
cy.get('@line').type(' \\alpha')
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
// the symbol completion should exist
|
||||
cy.findAllByRole('option', {
|
||||
name: /^\\alpha\s+𝛼\s+Greek$/,
|
||||
}).should('have.length', 1)
|
||||
})
|
||||
|
||||
it('does not display symbol completions in autocomplete when the feature is disabled', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
window.metaAttributesCache.set('ol-showSymbolPalette', false)
|
||||
const user = {
|
||||
id: '123abd',
|
||||
email: 'testuser@example.com',
|
||||
}
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders user={user} scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// type the name of a symbol
|
||||
cy.get('@line').type(' \\alpha')
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
|
||||
cy.findAllByRole('option', {
|
||||
name: /^\\alpha\s+𝛼\s+Greek$/,
|
||||
}).should('have.length', 0)
|
||||
})
|
||||
|
||||
it('displays environment completion when typing up to closing brace', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// Type \begin{itemize}.
|
||||
// Note: '{{}' is a single opening brace
|
||||
cy.get('@line').type('\\begin{{}itemize}', {
|
||||
delay: 100,
|
||||
})
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('displays environment completion when typing inside \\begin{}', function () {
|
||||
const scope = mockScope(mockDocContent('\\begin{}'))
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Put the cursor on a blank line above target line
|
||||
cy.get('.cm-line').eq(20).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// Move to the position between the braces then type 'itemize'
|
||||
cy.get('@line').type(`{downArrow}${'{rightArrow}'.repeat(7)}itemize`, {
|
||||
delay: 100,
|
||||
})
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('displays environment completion when typing after \\begin{', function () {
|
||||
const scope = mockScope(mockDocContent('\\begin{'))
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Put the cursor on a blank line above target line
|
||||
cy.get('.cm-line').eq(20).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// Move to the position after the opening brace then type 'itemize}'
|
||||
cy.get('@line').type(`{downArrow}${'{rightArrow}'.repeat(7)}itemize}`, {
|
||||
delay: 100,
|
||||
})
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('removes .tex but not .txt file extension from \\include and \\input', function () {
|
||||
const rootFolder: Folder[] = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'main.tex',
|
||||
},
|
||||
{
|
||||
_id: 'test-include-tex-doc',
|
||||
name: 'example.tex',
|
||||
},
|
||||
{
|
||||
_id: 'test-include-txt',
|
||||
name: 'sometext.txt',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Put the cursor on a blank line and type
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('\\include{e', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('example.tex').click()
|
||||
activeEditorLine().contains('\\include{example')
|
||||
activeEditorLine().type('}{Enter}')
|
||||
|
||||
activeEditorLine().type('\\include{s', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||
activeEditorLine().should('have.text', '\\include{sometext.txt}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().type('\\inclu', { delay: 100 })
|
||||
cy.contains('\\include{}').click()
|
||||
cy.contains('example.tex').click()
|
||||
activeEditorLine().should('have.text', '\\include{example}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().type('\\inclu', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('\\include{}').click()
|
||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||
activeEditorLine().should('have.text', '\\include{sometext.txt}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().click().as('line')
|
||||
activeEditorLine().type('\\input{e', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('example.tex').click()
|
||||
activeEditorLine().should('have.text', '\\input{example}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().click().as('line')
|
||||
activeEditorLine().type('\\input{s', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||
activeEditorLine().should('have.text', '\\input{sometext.txt}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().type('\\inpu', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('\\input{}').click()
|
||||
cy.findAllByRole('option').contains('example.tex').click()
|
||||
activeEditorLine().should('have.text', '\\input{example}')
|
||||
activeEditorLine().type('{Enter}')
|
||||
|
||||
activeEditorLine().type('\\inpu', { delay: 100 })
|
||||
cy.findAllByRole('option').contains('\\input{}').click()
|
||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||
activeEditorLine().should('have.text', '\\input{sometext.txt}')
|
||||
})
|
||||
|
||||
it('excludes the current command from completions', function () {
|
||||
const scope = mockScope(mockDocContent(''))
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(21).type('\\fff \\ff')
|
||||
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
cy.findAllByRole('option').contains('\\fff').click()
|
||||
|
||||
cy.get('.cm-line').eq(21).should('have.text', '\\fff \\fff')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,206 @@
|
||||
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'
|
||||
|
||||
describe('close brackets', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(20).as('active-line')
|
||||
cy.get('@active-line').click()
|
||||
})
|
||||
|
||||
describe('unprefixed characters', function () {
|
||||
it('auto-closes a curly bracket', function () {
|
||||
cy.get('@active-line').type('{{}')
|
||||
cy.get('@active-line').should('have.text', '{}')
|
||||
cy.get('@active-line').type('{backspace}')
|
||||
cy.get('@active-line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('auto-closes a square bracket', function () {
|
||||
cy.get('@active-line').type('[')
|
||||
cy.get('@active-line').should('have.text', '[]')
|
||||
cy.get('@active-line').type('{backspace}')
|
||||
cy.get('@active-line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('does not auto-close a round bracket', function () {
|
||||
cy.get('@active-line').type('(')
|
||||
cy.get('@active-line').should('have.text', '(')
|
||||
})
|
||||
|
||||
it('auto-closes a dollar sign', function () {
|
||||
cy.get('@active-line').type('$')
|
||||
cy.get('@active-line').should('have.text', '$$')
|
||||
cy.get('@active-line').type('{backspace}')
|
||||
cy.get('@active-line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('auto-closes another dollar sign', function () {
|
||||
cy.get('@active-line').type('$$')
|
||||
cy.get('@active-line').should('have.text', '$$$$')
|
||||
cy.get('@active-line').type('{backspace}{backspace}')
|
||||
cy.get('@active-line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('avoids creating an odd number of adjacent dollar signs', function () {
|
||||
cy.get('@active-line').type('$2')
|
||||
cy.get('@active-line').should('have.text', '$2$')
|
||||
cy.get('@active-line').type('{leftArrow}$')
|
||||
cy.get('@active-line').should('have.text', '$$2$')
|
||||
})
|
||||
})
|
||||
|
||||
describe('prefixed characters', function () {
|
||||
it('auto-closes a backslash-prefixed round bracket', function () {
|
||||
cy.get('@active-line').type('\\(')
|
||||
cy.get('@active-line').should('have.text', '\\(\\)')
|
||||
})
|
||||
|
||||
it('auto-closes a backslash-prefixed square bracket', function () {
|
||||
cy.get('@active-line').type('\\[')
|
||||
cy.get('@active-line').should('have.text', '\\[\\]')
|
||||
})
|
||||
|
||||
it('does not auto-close a backslash-prefixed curly bracket', function () {
|
||||
cy.get('@active-line').type('\\{{}')
|
||||
cy.get('@active-line').should('have.text', '\\{')
|
||||
})
|
||||
|
||||
it('does not auto-close a backslash-prefixed dollar sign', function () {
|
||||
cy.get('@active-line').type('\\$')
|
||||
cy.get('@active-line').should('have.text', '\\$')
|
||||
})
|
||||
})
|
||||
|
||||
describe('double-prefixed characters', function () {
|
||||
it('auto-closes a double-backslash-prefixed square bracket with a square bracket', function () {
|
||||
cy.get('@active-line').type('\\\\[')
|
||||
cy.get('@active-line').should('have.text', '\\\\[]')
|
||||
})
|
||||
|
||||
it('auto-closes a double-backslash-prefixed curly bracket with a curly bracket', function () {
|
||||
cy.get('@active-line').type('\\\\{')
|
||||
cy.get('@active-line').should('have.text', '\\\\{}')
|
||||
})
|
||||
|
||||
it('auto-closes a double-backslash-prefixed dollar sign with a dollar sign', function () {
|
||||
cy.get('@active-line').type('\\\\$')
|
||||
cy.get('@active-line').should('have.text', '\\\\$$')
|
||||
})
|
||||
|
||||
it('does not auto-close a double-backslash-prefixed round bracket', function () {
|
||||
cy.get('@active-line').type('\\\\(')
|
||||
cy.get('@active-line').should('have.text', '\\\\(')
|
||||
})
|
||||
})
|
||||
|
||||
describe('adjacent characters', function () {
|
||||
it('does auto-close a dollar sign before punctuation', function () {
|
||||
cy.get('@active-line').type(':2')
|
||||
cy.get('@active-line').type('{leftArrow}{leftArrow}$')
|
||||
cy.get('@active-line').should('have.text', '$$:2')
|
||||
})
|
||||
|
||||
it('does auto-close a dollar sign after punctuation', function () {
|
||||
cy.get('@active-line').type('2:')
|
||||
cy.get('@active-line').type('$')
|
||||
cy.get('@active-line').should('have.text', '2:$$')
|
||||
})
|
||||
|
||||
it('does not auto-close a dollar sign before text', function () {
|
||||
cy.get('@active-line').type('2')
|
||||
cy.get('@active-line').type('{leftArrow}$')
|
||||
cy.get('@active-line').should('have.text', '$2')
|
||||
})
|
||||
|
||||
it('does not auto-close a dollar sign after text', function () {
|
||||
cy.get('@active-line').type('2')
|
||||
cy.get('@active-line').type('$')
|
||||
cy.get('@active-line').should('have.text', '2$')
|
||||
})
|
||||
|
||||
it('does not auto-close a dollar sign before a command', function () {
|
||||
cy.get('@active-line').type('\\nu')
|
||||
cy.get('@active-line').type('{leftArrow}{leftArrow}{leftArrow}$')
|
||||
cy.get('@active-line').should('have.text', '$\\nu')
|
||||
})
|
||||
|
||||
it('does auto-close a dollar sign before a newline', function () {
|
||||
cy.get('@active-line').type('\\\\')
|
||||
cy.get('@active-line').type('{leftArrow}{leftArrow}$')
|
||||
cy.get('@active-line').should('have.text', '$$\\\\')
|
||||
})
|
||||
|
||||
it('does auto-close a curly bracket before punctuation', function () {
|
||||
cy.get('@active-line').type(':2')
|
||||
cy.get('@active-line').type('{leftArrow}{leftArrow}{{}')
|
||||
cy.get('@active-line').should('have.text', '{}:2')
|
||||
})
|
||||
|
||||
it('does auto-close a curly bracket after punctuation', function () {
|
||||
cy.get('@active-line').type('2:')
|
||||
cy.get('@active-line').type('{{}')
|
||||
cy.get('@active-line').should('have.text', '2:{}')
|
||||
})
|
||||
|
||||
it('does not auto-close a curly bracket before text', function () {
|
||||
cy.get('@active-line').type('2')
|
||||
cy.get('@active-line').type('{leftArrow}{{}')
|
||||
cy.get('@active-line').should('have.text', '{2')
|
||||
})
|
||||
|
||||
it('does auto-close a curly bracket after text', function () {
|
||||
cy.get('@active-line').type('2')
|
||||
cy.get('@active-line').type('{{}')
|
||||
cy.get('@active-line').should('have.text', '2{}')
|
||||
})
|
||||
|
||||
it('does auto-close $$ before punctuation', function () {
|
||||
cy.get('@active-line').type(':2')
|
||||
cy.get('@active-line').type('{leftArrow}{leftArrow}$$')
|
||||
cy.get('@active-line').should('have.text', '$$$$:2')
|
||||
})
|
||||
|
||||
it('does not auto-close $$ before text', function () {
|
||||
cy.get('@active-line').type('2')
|
||||
cy.get('@active-line').type('{leftArrow}$$')
|
||||
cy.get('@active-line').should('have.text', '$$2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('closed brackets', function () {
|
||||
it('does type over a closing dollar sign', function () {
|
||||
cy.get('@active-line').type('$2$')
|
||||
cy.get('@active-line').should('have.text', '$2$')
|
||||
})
|
||||
|
||||
it('does type over two closing dollar signs', function () {
|
||||
cy.get('@active-line').type('$$2$$')
|
||||
cy.get('@active-line').should('have.text', '$$2$$')
|
||||
})
|
||||
|
||||
it('does type over a closing curly bracket', function () {
|
||||
cy.get('@active-line').type('{{}2}')
|
||||
cy.get('@active-line').should('have.text', '{2}')
|
||||
})
|
||||
|
||||
it('does type over a closing square bracket', function () {
|
||||
cy.get('@active-line').type('[2]')
|
||||
cy.get('@active-line').should('have.text', '[2]')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
describe('Cursor and active line highlight', function () {
|
||||
const content = `line 1
|
||||
|
||||
${'long line '.repeat(200)}`
|
||||
|
||||
function assertIsFullLineHeight($item: JQuery<HTMLElement>) {
|
||||
cy.get('@line').then($line => {
|
||||
expect(Math.round($item.outerHeight()!)).to.equal(
|
||||
Math.round($line.outerHeight()!)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope(content)
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
})
|
||||
|
||||
it('has cursor', function () {
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.cm-cursor').as('cursor')
|
||||
|
||||
cy.get('.cm-cursor').then(assertIsFullLineHeight)
|
||||
})
|
||||
|
||||
it('has cursor on empty line whose height is the same as the line', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.cm-cursor').as('cursor')
|
||||
|
||||
cy.get('@cursor').then(assertIsFullLineHeight)
|
||||
})
|
||||
|
||||
it('has cursor on non-empty line whose height is the same as the line', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('wombat')
|
||||
|
||||
cy.get('.cm-cursor').as('cursor')
|
||||
|
||||
cy.get('@cursor').then(assertIsFullLineHeight)
|
||||
})
|
||||
|
||||
it('puts cursor in the correct place inside brackets', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('[{Enter}')
|
||||
|
||||
// Get the line inside the bracket
|
||||
cy.get('.cm-line').eq(2).as('line')
|
||||
|
||||
// Check that the middle of the cursor is within the line boundaries
|
||||
cy.get('.cm-cursor').then($cursor => {
|
||||
cy.get('@line').then($line => {
|
||||
const cursorCentreY = $cursor.offset()!.top + $cursor.outerHeight()! / 2
|
||||
const lineTop = $line.offset()!.top
|
||||
const lineBottom = lineTop + $line.outerHeight()!
|
||||
expect(cursorCentreY).to.be.within(lineTop, lineBottom)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('has active line highlight line decoration of same height as line when there is no selection and line does not wrap', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.cm-content .cm-activeLine').as('highlight')
|
||||
cy.get('.ol-cm-activeLineLayer .cm-activeLine').should('not.exist')
|
||||
|
||||
cy.get('@highlight').then(assertIsFullLineHeight)
|
||||
})
|
||||
|
||||
it('has active line highlight layer decoration of same height as non-wrapped line when there is no selection and line wraps', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(2).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.ol-cm-activeLineLayer .cm-activeLine').as('highlight')
|
||||
cy.get('.cm-content .cm-activeLine').should('not.exist')
|
||||
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
|
||||
cy.get('@highlight').then(assertIsFullLineHeight)
|
||||
})
|
||||
|
||||
it('has no active line highlight when there is a selection', function () {
|
||||
// Put the cursor on a blank line
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(isMac ? '{cmd}A' : '{ctrl}A')
|
||||
|
||||
cy.get('.cm-activeLine').should('not.exist')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,599 @@
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import { mockScope, rootFolderId } from '../helpers/mock-scope'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const clickToolbarButton = (text: string) => {
|
||||
cy.findByLabelText(text).click()
|
||||
cy.findByLabelText(text).trigger('mouseout')
|
||||
}
|
||||
|
||||
const chooseFileFromComputer = () => {
|
||||
cy.get('@file-input').selectFile(
|
||||
{
|
||||
fileName: 'frog.jpg',
|
||||
contents: Cypress.Buffer.from('image-data'),
|
||||
mimeType: 'image/jpg',
|
||||
},
|
||||
{
|
||||
force: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const matchUrl = (urlToMatch: RegExp | string) =>
|
||||
Cypress.sinon.match(req => {
|
||||
if (!req || typeof req.url !== 'string') {
|
||||
return false
|
||||
}
|
||||
if (typeof urlToMatch === 'string') {
|
||||
return req.url.endsWith(urlToMatch)
|
||||
}
|
||||
return Boolean(req.url.match(urlToMatch))
|
||||
})
|
||||
|
||||
describe('<FigureModal />', function () {
|
||||
// TODO: rewrite these tests to be in source mode when toolbar is added there
|
||||
// TODO: Write tests for width toggle, when we can match on source code
|
||||
function mount() {
|
||||
const content = ''
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
const FileTreePathProvider: FC = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
dirname: cy.stub(),
|
||||
findEntityByPath: cy.stub(),
|
||||
pathInFolder: cy.stub(),
|
||||
previewByPath: cy
|
||||
.stub()
|
||||
.as('previewByPath')
|
||||
.returns({ url: 'frog.jpg', extension: 'jpg' }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptMathJax()
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
|
||||
mount()
|
||||
})
|
||||
|
||||
describe('Upload from computer source', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptFileUpload()
|
||||
clickToolbarButton('Insert Figure')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('Upload from computer').click()
|
||||
})
|
||||
cy.findByLabelText('Uppy Dashboard')
|
||||
.get('.uppy-Dashboard-input:first')
|
||||
.as('file-input')
|
||||
})
|
||||
|
||||
it('Shows file name and size when selecting file', function () {
|
||||
chooseFileFromComputer()
|
||||
cy.findByLabelText('File name').should('have.text', 'frog.jpg')
|
||||
cy.findByLabelText('File size').should('have.text', '10 B')
|
||||
})
|
||||
|
||||
it('Uploads file when clicking insert', function () {
|
||||
chooseFileFromComputer()
|
||||
cy.get('@uploadRequest').should('not.have.been.called')
|
||||
cy.findByText('Insert figure').click()
|
||||
cy.get('@uploadRequest').should(
|
||||
'have.been.calledWith',
|
||||
matchUrl(`/project/test-project/upload?folder_id=${rootFolderId}`)
|
||||
)
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{Enter Caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Enables insert button when choosing file', function () {
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.disabled')
|
||||
chooseFileFromComputer()
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.enabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Upload from project files source', function () {
|
||||
beforeEach(function () {
|
||||
clickToolbarButton('Insert Figure')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('From project files').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Lists files from project', function () {
|
||||
cy.findByRole('textbox', { name: 'Image file' }).click()
|
||||
cy.findByRole('listbox')
|
||||
.children()
|
||||
.should('have.length', 2)
|
||||
.should('contain.text', 'frog.jpg')
|
||||
.should('contain.text', 'unicorn.png')
|
||||
.should('contain.text', 'figures/')
|
||||
})
|
||||
|
||||
it('Enables insert button when choosing file', function () {
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.disabled')
|
||||
cy.findByRole('textbox', { name: 'Image file' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByText('frog.jpg').click()
|
||||
})
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.enabled')
|
||||
})
|
||||
|
||||
it('Inserts file when pressing insert button', function () {
|
||||
cy.findByRole('textbox', { name: 'Image file' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByText('frog.jpg').click()
|
||||
})
|
||||
cy.findByRole('button', { name: 'Insert figure' }).click()
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{Enter Caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('From another project source', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptProjectListing()
|
||||
cy.interceptCompile()
|
||||
cy.interceptLinkedFile()
|
||||
clickToolbarButton('Insert Figure')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('button', { name: 'From another project' }).click()
|
||||
})
|
||||
cy.findByRole('textbox', { name: 'Project' }).as('project-dropdown')
|
||||
cy.findByRole('textbox', { name: 'Image file' }).as('file-dropdown')
|
||||
})
|
||||
|
||||
it('List projects and files in projects', function () {
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.disabled')
|
||||
cy.get('@file-dropdown').should('be.disabled')
|
||||
cy.get('@project-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findAllByRole('option').should('have.length', 2)
|
||||
cy.findByRole('option', { name: 'My first project' }).click()
|
||||
})
|
||||
cy.get('@file-dropdown').should('be.enabled')
|
||||
cy.get('@file-dropdown').click()
|
||||
cy.findByRole('listbox').as('file-select')
|
||||
cy.get('@file-select').children().should('have.length', 2)
|
||||
cy.get('@file-select').should('contain.text', 'frog.jpg')
|
||||
cy.get('@file-select').should('contain.text', 'figures/unicorn.png')
|
||||
cy.get('@file-select').within(() => {
|
||||
cy.findByText('frog.jpg').click()
|
||||
})
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.enabled')
|
||||
})
|
||||
|
||||
it('Enables insert button when choosing file', function () {
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.disabled')
|
||||
cy.get('@project-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'My first project' }).click()
|
||||
})
|
||||
cy.get('@file-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'frog.jpg' }).click()
|
||||
})
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.enabled')
|
||||
})
|
||||
|
||||
it('Creates linked file when pressing insert', function () {
|
||||
cy.get('@project-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'My first project' }).click()
|
||||
})
|
||||
cy.get('@file-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByText('frog.jpg').click()
|
||||
})
|
||||
cy.findByText('Insert figure').click()
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'project_file',
|
||||
data: {
|
||||
source_entity_path: '/frog.jpg',
|
||||
source_project_id: 'fake-project-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{Enter Caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Creates linked output file when pressing insert', function () {
|
||||
cy.get('@project-dropdown').click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'My first project' }).click()
|
||||
})
|
||||
cy.findByRole('button', { name: 'select from output files' }).click()
|
||||
cy.findByRole('textbox', { name: 'Output file' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'output.pdf' }).click()
|
||||
})
|
||||
cy.findByText('Insert figure').click()
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'project_output_file',
|
||||
data: {
|
||||
source_output_file_path: 'output.pdf',
|
||||
source_project_id: 'fake-project-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{Enter Caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Feature flags', function () {
|
||||
describe('with hasLinkUrlFeature=false', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasLinkedProjectFileFeature: true,
|
||||
hasLinkedProjectOutputFileFeature: true,
|
||||
hasLinkUrlFeature: false,
|
||||
})
|
||||
mount()
|
||||
clickToolbarButton('Insert Figure')
|
||||
})
|
||||
it('should not have import from url option', function () {
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('From URL').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('with hasLinkedProjectFileFeature=false and hasLinkedProjectOutputFileFeature=false', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasLinkedProjectFileFeature: false,
|
||||
hasLinkedProjectOutputFileFeature: false,
|
||||
hasLinkUrlFeature: true,
|
||||
})
|
||||
mount()
|
||||
clickToolbarButton('Insert Figure')
|
||||
})
|
||||
it('should not have import from project file option', function () {
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('From another project').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function setupFromAnotherProject() {
|
||||
mount()
|
||||
cy.interceptProjectListing()
|
||||
clickToolbarButton('Insert Figure')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('From another project').click()
|
||||
})
|
||||
cy.findByRole('textbox', { name: 'Project' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'My first project' }).click()
|
||||
})
|
||||
}
|
||||
function expectNoOutputSwitch() {
|
||||
it('should hide output switch', function () {
|
||||
cy.findByText('select from output files').should('not.exist')
|
||||
cy.findByText('select from source files').should('not.exist')
|
||||
})
|
||||
}
|
||||
|
||||
describe('with hasLinkedProjectFileFeature=false', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasLinkedProjectFileFeature: false,
|
||||
hasLinkedProjectOutputFileFeature: true,
|
||||
hasLinkUrlFeature: true,
|
||||
})
|
||||
cy.interceptCompile()
|
||||
setupFromAnotherProject()
|
||||
})
|
||||
expectNoOutputSwitch()
|
||||
it('should show output file selector', function () {
|
||||
cy.findByRole('textbox', { name: 'Output file' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'output.pdf' }).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with hasLinkedProjectOutputFileFeature=false', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasLinkedProjectFileFeature: true,
|
||||
hasLinkedProjectOutputFileFeature: false,
|
||||
hasLinkUrlFeature: true,
|
||||
})
|
||||
setupFromAnotherProject()
|
||||
})
|
||||
expectNoOutputSwitch()
|
||||
|
||||
it('should show source file selector', function () {
|
||||
cy.findByRole('textbox', { name: 'Image file' }).click()
|
||||
cy.findByRole('listbox').within(() => {
|
||||
cy.findByRole('option', { name: 'frog.jpg' }).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('From URL source', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptLinkedFile()
|
||||
clickToolbarButton('Insert Figure')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('From URL').click()
|
||||
})
|
||||
cy.findByLabelText('File name in this project').as(
|
||||
'relocated-file-name-input'
|
||||
)
|
||||
cy.findByLabelText('Image URL').as('image-url-input')
|
||||
cy.findByRole('checkbox', { name: 'Include label' }).as(
|
||||
'include-label-checkbox'
|
||||
)
|
||||
cy.findByRole('checkbox', { name: 'Include caption' }).as(
|
||||
'include-caption-checkbox'
|
||||
)
|
||||
})
|
||||
|
||||
it('Auto fills name based on url', function () {
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.get('@relocated-file-name-input').should('have.value', 'frog.jpg')
|
||||
cy.get('@relocated-file-name-input').type('pig')
|
||||
cy.get('@relocated-file-name-input').should('have.value', 'pig.jpg')
|
||||
})
|
||||
|
||||
it('Enables insert button when name and url is available', function () {
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.disabled')
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.findByRole('button', { name: 'Insert figure' }).should('be.enabled')
|
||||
})
|
||||
|
||||
it('Adds linked file when pressing insert', function () {
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.findByRole('button', { name: 'Insert figure' }).click()
|
||||
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'url',
|
||||
data: {
|
||||
url: 'https://my-fake-website.com/frog.jpg',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{Enter Caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Selects the caption when the figure is inserted with a caption', function () {
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.findByRole('button', { name: 'Insert figure' }).click()
|
||||
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'url',
|
||||
data: {
|
||||
url: 'https://my-fake-website.com/frog.jpg',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-selectionLayer .cm-selectionBackground').should(
|
||||
'have.length',
|
||||
1
|
||||
)
|
||||
|
||||
// If caption is selected then typing will replace the whole caption
|
||||
cy.focused().type('My caption')
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\caption{My caption} 🏷fig:enter-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Selects the label when the figure is inserted without a caption', function () {
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.get('@include-caption-checkbox').uncheck()
|
||||
cy.findByRole('button', { name: 'Insert figure' }).click()
|
||||
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'url',
|
||||
data: {
|
||||
url: 'https://my-fake-website.com/frog.jpg',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-selectionLayer .cm-selectionBackground').should(
|
||||
'have.length',
|
||||
1
|
||||
)
|
||||
|
||||
// If label is selected then typing will replace the whole label
|
||||
cy.focused().type('fig:my-label')
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering \\label{fig:my-label}\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Places the cursor after the figure if it is inserted without a caption or a label', function () {
|
||||
cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
|
||||
cy.get('@include-caption-checkbox').uncheck()
|
||||
cy.get('@include-label-checkbox').uncheck()
|
||||
|
||||
cy.findByRole('button', { name: 'Insert figure' }).click()
|
||||
|
||||
cy.get('@linked-file-request').should('have.been.calledWithMatch', {
|
||||
body: {
|
||||
provider: 'url',
|
||||
data: {
|
||||
url: 'https://my-fake-website.com/frog.jpg',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering\\end{figure}'
|
||||
)
|
||||
|
||||
cy.focused().type('Some more text')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure} \\centering\\end{figure}Some more text'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Editing existing figure', function () {
|
||||
it('Parses existing label and caption', function () {
|
||||
cy.get('.cm-content').type(
|
||||
`\\begin{{}figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.5\\linewidth]{{}frog.jpg}
|
||||
\\caption{{}My caption}
|
||||
\\label{{}fig:my-label}
|
||||
\\end{{}figure}`,
|
||||
{ delay: 0 }
|
||||
)
|
||||
cy.get('[aria-label="Edit figure"]').click()
|
||||
cy.findByRole('checkbox', { name: 'Include caption' }).should(
|
||||
'be.checked'
|
||||
)
|
||||
cy.findByRole('checkbox', { name: 'Include label' }).should('be.checked')
|
||||
})
|
||||
|
||||
it('Parses existing width', function () {
|
||||
cy.get('.cm-content').type(
|
||||
`\\begin{{}figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
|
||||
\\caption{{}My caption}
|
||||
\\label{{}fig:my-label}
|
||||
\\end{{}figure}`,
|
||||
{ delay: 0 }
|
||||
)
|
||||
cy.get('[aria-label="Edit figure"]').click()
|
||||
cy.get('[value="0.75"]').should('be.checked')
|
||||
})
|
||||
|
||||
it('Removes existing label when unchecked', function () {
|
||||
cy.get('.cm-content').type(
|
||||
`\\begin{{}figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
|
||||
\\label{{}fig:my-label}
|
||||
\\end{{}figure}`,
|
||||
{ delay: 0 }
|
||||
)
|
||||
cy.get('[aria-label="Edit figure"]').click()
|
||||
cy.findByRole('checkbox', { name: 'Include label' }).click()
|
||||
cy.findByRole('checkbox', { name: 'Include label' }).should(
|
||||
'not.be.checked'
|
||||
)
|
||||
cy.findByText('Done').click()
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure}\\centering\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Removes existing caption when unchecked', function () {
|
||||
cy.get('.cm-content').type(
|
||||
`\\begin{{}figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
|
||||
\\caption{{}My caption}
|
||||
\\label{{}fig:my-label}
|
||||
\\end{{}figure}`,
|
||||
{ delay: 0 }
|
||||
)
|
||||
cy.get('[aria-label="Edit figure"]').click()
|
||||
cy.findByRole('checkbox', { name: 'Include caption' }).click()
|
||||
cy.findByRole('checkbox', { name: 'Include caption' }).should(
|
||||
'not.be.checked'
|
||||
)
|
||||
cy.findByText('Done').click()
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
'\\begin{figure}\\centering🏷fig:my-label\\end{figure}'
|
||||
)
|
||||
})
|
||||
|
||||
it('Preserves other content when removing figure', function () {
|
||||
cy.get('.cm-content').type(
|
||||
`text above
|
||||
\\begin{{}figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
|
||||
\\caption{{}My caption}
|
||||
\\label{{}fig:my-label}
|
||||
\\end{{}figure}
|
||||
text below`,
|
||||
{ delay: 0 }
|
||||
)
|
||||
cy.get('[aria-label="Edit figure"]').click()
|
||||
cy.findByRole('button', { name: 'Remove or replace figure' }).click()
|
||||
cy.findByText('Delete figure').click()
|
||||
cy.get('.cm-content').should('have.text', 'text abovetext below')
|
||||
})
|
||||
|
||||
it('Opens figure modal on pasting image', function () {
|
||||
cy.fixture<Uint8Array>('images/gradient.png').then(gradientBuffer => {
|
||||
const gradientFile = new File([gradientBuffer], 'gradient.png', {
|
||||
type: 'image/png',
|
||||
})
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.items.add(gradientFile)
|
||||
cy.wrap(clipboardData.files).should('have.length', 1)
|
||||
cy.get('.cm-content').trigger('paste', { clipboardData })
|
||||
cy.findByText('Upload from computer').should('be.visible')
|
||||
cy.findByLabelText('File name in this project').should(
|
||||
'have.value',
|
||||
'gradient.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Add tests for replacing image when we can match on image path
|
||||
// TODO: Add tests for changing image size when we can match on figure width
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
describe('<CodeMirrorEditor/> fundamentals', function () {
|
||||
const content = `
|
||||
test
|
||||
`
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope(content)
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(0).as('first-line')
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('.cm-line').eq(2).as('empty-line')
|
||||
|
||||
cy.get('@line').click()
|
||||
})
|
||||
|
||||
it('deletes with backspace', function () {
|
||||
cy.get('@line').type('{backspace}')
|
||||
cy.get('@line').should('have.text', 'tes')
|
||||
})
|
||||
|
||||
it('moves with arrow keys', function () {
|
||||
cy.get('@line').type('{leftArrow}1')
|
||||
cy.get('@line').should('have.text', 'tes1t')
|
||||
cy.get('@line').type('{rightArrow}2')
|
||||
cy.get('@line').should('have.text', 'tes1t2')
|
||||
cy.get('@line').type('{downArrow}3')
|
||||
cy.get('@line').type('{upArrow}{upArrow}4')
|
||||
cy.get('@empty-line').should('have.text', '3')
|
||||
cy.get('@first-line').should('have.text', '4')
|
||||
})
|
||||
|
||||
it('deletes with delete', function () {
|
||||
cy.get('@line').type('{leftArrow}{del}')
|
||||
cy.get('@line').should('have.text', 'tes')
|
||||
})
|
||||
|
||||
it('types characters', function () {
|
||||
cy.get('@empty-line').type('hello codemirror!')
|
||||
cy.get('@empty-line').should('have.text', 'hello codemirror!')
|
||||
})
|
||||
|
||||
it('replaces selections', function () {
|
||||
cy.get('@line').type('{shift}{leftArrow}{leftArrow}{leftArrow}')
|
||||
cy.get('@line').type('abby cat')
|
||||
cy.get('@line').should('have.text', 'tabby cat')
|
||||
})
|
||||
|
||||
it('inserts LaTeX commands', function () {
|
||||
cy.get('@empty-line').type('\\cmd[opt]{{}arg}')
|
||||
cy.get('@empty-line').should('have.text', '\\cmd[opt]{arg}')
|
||||
})
|
||||
|
||||
it('allows line-breaks', function () {
|
||||
cy.get('.cm-content').find('.cm-line').should('have.length', 3)
|
||||
cy.get('@empty-line').type('{enter}{enter}')
|
||||
cy.get('.cm-content').find('.cm-line').should('have.length', 5)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,280 @@
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { metaKey } from '../helpers/meta-key'
|
||||
import { activeEditorLine } from '../helpers/active-editor-line'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const CHARACTERS =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\\0123456789'
|
||||
|
||||
describe('keyboard shortcuts', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('.cm-editor').as('editor')
|
||||
})
|
||||
|
||||
it('comment line with {meta+/}', function () {
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@line').type(`{${metaKey}+/}`)
|
||||
cy.get('@line').should('have.text', '% text')
|
||||
|
||||
cy.get('@line').type(`{${metaKey}+/}`)
|
||||
cy.get('@line').should('have.text', 'text')
|
||||
})
|
||||
|
||||
it('comment line with {ctrl+#}', function () {
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@editor').trigger('keydown', { key: '#', ctrlKey: true })
|
||||
cy.get('@line').should('have.text', '% text')
|
||||
|
||||
cy.get('@editor').trigger('keydown', { key: '#', ctrlKey: true })
|
||||
cy.get('@line').should('have.text', 'text')
|
||||
})
|
||||
|
||||
it('undo line with {meta+z}', function () {
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@line').type(`{${metaKey}+z}`)
|
||||
cy.get('@line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('redo line with {meta+shift+z}', function () {
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@line').type(`{${metaKey}+z}`) // undo
|
||||
cy.get('@line').type(`{${metaKey}+shift+z}`) // redo
|
||||
cy.get('@line').should('have.text', 'text')
|
||||
})
|
||||
|
||||
it('redo line with {meta+y}', function () {
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@line').type(`{${metaKey}+z}`) // undo
|
||||
cy.get('@line').type(`{${metaKey}+y}`) // redo
|
||||
cy.get('@line').should('have.text', 'text')
|
||||
})
|
||||
|
||||
it('delete line with {meta+d}', function () {
|
||||
cy.get('.cm-line').then($lines => {
|
||||
const linesCount = $lines.length
|
||||
cy.get('@line').type(`{${metaKey}+d}`)
|
||||
cy.get('.cm-line').should('have.length', linesCount - 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('indent line with {tab}', function () {
|
||||
cy.get('@line').trigger('keydown', { key: 'Tab' })
|
||||
cy.get('@line').should('have.text', ' ')
|
||||
})
|
||||
|
||||
it('unindent line with {shift+tab}', function () {
|
||||
cy.get('@line').trigger('keydown', { key: 'Tab' }) // indent
|
||||
cy.get('@line').trigger('keydown', { key: 'Tab', shiftKey: true }) // unindent
|
||||
cy.get('@line').should('have.text', '')
|
||||
})
|
||||
|
||||
it('uppercase selection with {ctrl+u}', function () {
|
||||
cy.get('@line').type('a')
|
||||
cy.get('@line').type('{shift+leftArrow}') // select text
|
||||
cy.get('@line').type('{ctrl+u}')
|
||||
cy.get('@line').should('have.text', 'A')
|
||||
})
|
||||
|
||||
it('lowercase selection with {ctrl+shift+u}', function () {
|
||||
if (navigator.platform.startsWith('Linux')) {
|
||||
// Skip test as {ctrl+shift+u} is bound elsewhere in some Linux systems
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
this.skip()
|
||||
}
|
||||
|
||||
cy.get('@line').type('A')
|
||||
cy.get('@line').type('{shift+leftArrow}') // select text
|
||||
cy.get('@line').type('{ctrl+shift+u}') // TODO: ctrl+shift+u is a system shortcut so this fails in CI
|
||||
cy.get('@line').should('have.text', 'a')
|
||||
})
|
||||
|
||||
it('wrap selection with "\\textbf{}" by using {meta+b}', function () {
|
||||
cy.get('@line').type('a')
|
||||
cy.get('@line').type('{shift+leftArrow}') // select text
|
||||
cy.get('@line').type(`{${metaKey}+b}`)
|
||||
cy.get('@line').should('have.text', '\\textbf{a}')
|
||||
})
|
||||
|
||||
it('wrap selection with "\\textit{}" by using {meta+i}', function () {
|
||||
cy.get('@line').type('a')
|
||||
cy.get('@line').type('{shift+leftArrow}') // select text
|
||||
cy.get('@line').type(`{${metaKey}+i}`)
|
||||
cy.get('@line').should('have.text', '\\textit{a}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('emacs keybindings', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
|
||||
const shortDoc = `
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
contentLine1
|
||||
contentLine2
|
||||
contentLine3
|
||||
\\end{document}`
|
||||
|
||||
const scope = mockScope(shortDoc)
|
||||
const userSettings = { mode: 'emacs' }
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').scrollIntoView()
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.cm-editor').as('editor')
|
||||
})
|
||||
|
||||
it('emulates search behaviour', function () {
|
||||
activeEditorLine().should('have.text', '\\documentclass{article}')
|
||||
|
||||
// Search should be closed
|
||||
cy.findByRole('search').should('have.length', 0)
|
||||
|
||||
// Invoke C-s
|
||||
cy.get('@line').type('{ctrl}s')
|
||||
|
||||
// Search should now be open
|
||||
cy.findByRole('search').should('have.length', 1)
|
||||
cy.findByRole('textbox', { name: 'Find' }).as('search-input')
|
||||
|
||||
// Write a search query
|
||||
cy.get('@search-input').should('have.focus').type('contentLine')
|
||||
cy.contains(`1 of 3`)
|
||||
// Should assert that activeEditorLine.index() === 21, but activeEditorLine
|
||||
// only works if editor is focused, not the search box.
|
||||
|
||||
// Repeated C-s should go to next match
|
||||
cy.get('@search-input').type('{ctrl}s')
|
||||
cy.contains(`2 of 3`)
|
||||
// Should assert that activeEditorLine.index() === 22, but activeEditorLine
|
||||
// only works if editor is focused, not the search box.
|
||||
|
||||
// C-g should close the search
|
||||
cy.get('@search-input').type('{ctrl}g')
|
||||
cy.findByRole('search').should('have.length', 0)
|
||||
|
||||
// Cursor should be back to where the search originated from
|
||||
activeEditorLine().should('have.text', '\\documentclass{article}')
|
||||
|
||||
// Invoke C-r
|
||||
cy.get('@line').type('{ctrl}r')
|
||||
|
||||
// Search should now be open at first match
|
||||
cy.findByRole('search').should('have.length', 1)
|
||||
cy.contains(`0 of 3`)
|
||||
|
||||
// Repeated C-r should go to previous match
|
||||
cy.get('@search-input').type('{ctrl}r')
|
||||
cy.contains(`3 of 3`)
|
||||
|
||||
// Close search panel to clear global variable
|
||||
cy.get('@search-input').type('{ctrl}g')
|
||||
cy.findByRole('search').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('toggle comments with M-;', function () {
|
||||
cy.get('@line').should('have.text', '\\documentclass{article}')
|
||||
cy.get('@line').type('{alt};')
|
||||
cy.get('@line').should('have.text', '% \\documentclass{article}')
|
||||
})
|
||||
|
||||
it('should jump between start and end with M-S-, and M-S-.', function () {
|
||||
activeEditorLine().should('have.text', '\\documentclass{article}')
|
||||
activeEditorLine().type('{alt}{shift},')
|
||||
activeEditorLine().should('have.text', '')
|
||||
activeEditorLine().type('{alt}{shift}.')
|
||||
activeEditorLine().should('have.text', '\\end{document}')
|
||||
})
|
||||
|
||||
it('can enter characters', function () {
|
||||
cy.get('.cm-line').eq(0).as('line')
|
||||
cy.get('@line').scrollIntoView()
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(CHARACTERS)
|
||||
cy.get('@line').should('have.text', CHARACTERS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('vim keybindings', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
|
||||
// Make a short doc that will fit entirely into the dom tree, so that
|
||||
// index() corresponds to line number - 1
|
||||
const shortDoc = `
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
contentLine1
|
||||
contentLine2
|
||||
contentLine3
|
||||
\\end{document}
|
||||
`
|
||||
|
||||
const scope = mockScope(shortDoc)
|
||||
const userSettings = { mode: 'vim' }
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').scrollIntoView()
|
||||
cy.get('@line').click()
|
||||
cy.get('.cm-editor').as('editor')
|
||||
})
|
||||
|
||||
it('can enter characters', function () {
|
||||
cy.get('.cm-line').eq(0).as('line')
|
||||
cy.get('@line').scrollIntoView()
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(`i${CHARACTERS}{esc}`)
|
||||
cy.get('@line').should('have.text', CHARACTERS)
|
||||
})
|
||||
|
||||
it('can move around in normal mode', function () {
|
||||
// Move cursor up
|
||||
cy.get('@line').type('k')
|
||||
activeEditorLine().should('have.text', '')
|
||||
|
||||
// Move cursor down
|
||||
cy.get('@line').type('j')
|
||||
activeEditorLine().should('have.text', '\\begin{document}')
|
||||
|
||||
// Move the cursor left, insert 1, move it right, insert a 2
|
||||
cy.get('@line').type('hi1{esc}la2{esc}')
|
||||
cy.get('@line').should('have.text', '\\documentclass{article1}2')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,157 @@
|
||||
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: 'af', dic: 'af_ZA', name: 'Afrikaans' },
|
||||
{ code: 'an', dic: 'an_ES', name: 'Aragonese' },
|
||||
{ code: 'ar', dic: 'ar', name: 'Arabic' },
|
||||
{ code: 'be_BY', dic: 'be_BY', name: 'Belarusian' },
|
||||
{ code: 'bg', dic: 'bg_BG', name: 'Bulgarian' },
|
||||
{ code: 'bn_BD', dic: 'bn_BD', name: 'Bengali' },
|
||||
{ code: 'bo', dic: 'bo', name: 'Tibetan' },
|
||||
{ code: 'br', dic: 'br_FR', name: 'Breton' },
|
||||
{ code: 'bs_BA', dic: 'bs_BA', name: 'Bosnian' },
|
||||
{ code: 'ca', dic: 'ca', name: 'Catalan' },
|
||||
{ code: 'cs', dic: 'cs_CZ', name: 'Czech' },
|
||||
{ code: 'de', dic: 'de_DE', name: 'German' },
|
||||
{ code: 'de_AT', dic: 'de_AT', name: 'German (Austria)' },
|
||||
{ code: 'de_CH', dic: 'de_CH', name: 'German (Switzerland)' },
|
||||
{ code: 'dz', dic: 'dz', name: 'Dzongkha' },
|
||||
{ code: 'el', dic: 'el_GR', name: 'Greek' },
|
||||
{ code: 'en_AU', dic: 'en_AU', name: 'English (Australian)' },
|
||||
{ code: 'en_CA', dic: 'en_CA', name: 'English (Canadian)' },
|
||||
{ code: 'en_GB', dic: 'en_GB', name: 'English (British)' },
|
||||
{ code: 'en_US', dic: 'en_US', name: 'English (American)' },
|
||||
{ code: 'en_ZA', dic: 'en_ZA', name: 'English (South African)' },
|
||||
{ code: 'eo', dic: 'eo', name: 'Esperanto' },
|
||||
{ code: 'es', dic: 'es_ES', name: 'Spanish' },
|
||||
{ code: 'et', dic: 'et_EE', name: 'Estonian' },
|
||||
{ code: 'eu', dic: 'eu', name: 'Basque' },
|
||||
{ code: 'fa', dic: 'fa_IR', name: 'Persian' },
|
||||
{ code: 'fo', dic: 'fo', name: 'Faroese' },
|
||||
{ code: 'fr', dic: 'fr', name: 'French' },
|
||||
{ code: 'ga', dic: 'ga_IE', name: 'Irish' },
|
||||
{ code: 'gd_GB', dic: 'gd_GB', name: 'Scottish Gaelic' },
|
||||
{ code: 'gl', dic: 'gl_ES', name: 'Galician' },
|
||||
{ code: 'gu_IN', dic: 'gu_IN', name: 'Gujarati' },
|
||||
{ code: 'gug_PY', dic: 'gug_PY', name: 'Guarani' },
|
||||
{ code: 'he_IL', dic: 'he_IL', name: 'Hebrew' },
|
||||
{ code: 'hi_IN', dic: 'hi_IN', name: 'Hindi' },
|
||||
{ code: 'hr', dic: 'hr_HR', name: 'Croatian' },
|
||||
{ code: 'hu_HU', dic: 'hu_HU', name: 'Hungarian' },
|
||||
{ code: 'id', dic: 'id_ID', name: 'Indonesian' },
|
||||
{ code: 'is_IS', dic: 'is_IS', name: 'Icelandic' },
|
||||
{ code: 'it', dic: 'it_IT', name: 'Italian' },
|
||||
{ code: 'kk', dic: 'kk_KZ', name: 'Kazakh' },
|
||||
{ code: 'kmr', dic: 'kmr_Latn', name: 'Kurmanji' },
|
||||
{ code: 'ko', dic: 'ko', name: 'Korean' },
|
||||
{ code: 'lo_LA', dic: 'lo_LA', name: 'Laotian' },
|
||||
{ code: 'lt', dic: 'lt_LT', name: 'Lithuanian' },
|
||||
{ code: 'lv', dic: 'lv_LV', name: 'Latvian' },
|
||||
{ code: 'ml_IN', dic: 'ml_IN', name: 'Malayalam' },
|
||||
{ code: 'mn_MN', dic: 'mn_MN', name: 'Mongolian' },
|
||||
{ code: 'nb_NO', dic: 'nb_NO', name: 'Norwegian (Bokmål)' },
|
||||
{ code: 'ne_NP', dic: 'ne_NP', name: 'Nepali' },
|
||||
{ code: 'nl', dic: 'nl', name: 'Dutch' },
|
||||
{ code: 'nn_NO', dic: 'nn_NO', name: 'Norwegian (Nynorsk)' },
|
||||
{ code: 'oc_FR', dic: 'oc_FR', name: 'Occitan' },
|
||||
{ code: 'pl', dic: 'pl_PL', name: 'Polish' },
|
||||
{ code: 'pt_BR', dic: 'pt_BR', name: 'Portuguese (Brazilian)' },
|
||||
{ code: 'pt_PT', dic: 'pt_PT', name: 'Portuguese (European)' },
|
||||
{ code: 'ro', dic: 'ro_RO', name: 'Romanian' },
|
||||
{ code: 'ru', dic: 'ru_RU', name: 'Russian' },
|
||||
{ code: 'si_LK', dic: 'si_LK', name: 'Sinhala' },
|
||||
{ code: 'sk', dic: 'sk_SK', name: 'Slovak' },
|
||||
{ code: 'sl', dic: 'sl_SI', name: 'Slovenian' },
|
||||
{ code: 'sr_RS', dic: 'sr_RS', name: 'Serbian' },
|
||||
{ code: 'sv', dic: 'sv_SE', name: 'Swedish' },
|
||||
{ code: 'sw_TZ', dic: 'sw_TZ', name: 'Swahili' },
|
||||
{ code: 'te_IN', dic: 'te_IN', name: 'Telugu' },
|
||||
{ code: 'th_TH', dic: 'th_TH', name: 'Thai' },
|
||||
{ code: 'tl', dic: 'tl', name: 'Tagalog' },
|
||||
{ code: 'tr_TR', dic: 'tr_TR', name: 'Turkish' },
|
||||
{ code: 'uz_UZ', dic: 'uz_UZ', name: 'Uzbek' },
|
||||
{ code: 'vi_VN', dic: 'vi_VN', name: 'Vietnamese' },
|
||||
]
|
||||
|
||||
const suggestions = {
|
||||
af: ['medicyne', 'medisyne'],
|
||||
be_BY: ['лекi', 'лекі'],
|
||||
bg: ['лекарствo', 'лекарство'],
|
||||
de: ['Medicin', 'Medizin'],
|
||||
en_CA: ['theatr', 'theatre'],
|
||||
en_GB: ['medecine', 'medicine'],
|
||||
en_US: ['theatr', 'theater'],
|
||||
es: ['medicaminto', 'medicamento'],
|
||||
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-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} ${to}`)
|
||||
cy.get('@line')
|
||||
.get('.ol-cm-spelling-error', { timeout: 10000 })
|
||||
.should('have.length', 1)
|
||||
cy.get('@line').get('.ol-cm-spelling-error').should('have.text', from)
|
||||
|
||||
cy.get('@line').get('.ol-cm-spelling-error').rightclick()
|
||||
cy.findByRole('menuitem', { name: to }).click()
|
||||
cy.get('@line').contains(`${to} ${to}`)
|
||||
cy.get('@line').find('.ol-cm-spelling-error').should('not.exist')
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,787 @@
|
||||
// Needed since eslint gets confused by mocha-each
|
||||
/* eslint-disable mocha/prefer-arrow-callback */
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import forEach from 'mocha-each'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const mountEditor = (content: string | string[]) => {
|
||||
if (Array.isArray(content)) {
|
||||
content = content.join('\n')
|
||||
}
|
||||
if (!content.startsWith('\n')) {
|
||||
content = '\n' + content
|
||||
}
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
cy.viewport(1000, 800)
|
||||
cy.mount(
|
||||
<TestContainer style={{ width: 1000, height: 800 }}>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
cy.get('.cm-line').first().click()
|
||||
}
|
||||
|
||||
function checkTable(
|
||||
expected: (string | { text: string; colspan: number })[][]
|
||||
) {
|
||||
cy.get('.table-generator').as('table').should('exist')
|
||||
cy.get('@table')
|
||||
.find('tbody')
|
||||
.as('body')
|
||||
.find('tr')
|
||||
.should('have.length', expected.length)
|
||||
cy.get('@body')
|
||||
.find('tr')
|
||||
.each((row, rowIndex) => {
|
||||
// Add one to the expected length to account for the row selector
|
||||
cy.wrap(row)
|
||||
.find('.table-generator-cell')
|
||||
.as('cells')
|
||||
.should('have.length', expected[rowIndex].length)
|
||||
cy.get('@cells').each((cell, cellIndex) => {
|
||||
const expectation = expected[rowIndex][cellIndex]
|
||||
const cellText =
|
||||
typeof expectation === 'string' ? expectation : expectation.text
|
||||
const colspan =
|
||||
typeof expectation === 'string' ? undefined : expectation.colspan
|
||||
cy.wrap(cell).should('contain.text', cellText)
|
||||
if (colspan) {
|
||||
cy.wrap(cell).should('have.attr', 'colspan', colspan.toString())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function checkBordersWithNoMultiColumn(
|
||||
verticalBorderIndices: boolean[],
|
||||
horizontalBorderIndices: boolean[]
|
||||
) {
|
||||
cy.get('.table-generator').as('table').should('have.length', 1)
|
||||
cy.get('@table')
|
||||
.find('tbody')
|
||||
.as('body')
|
||||
.find('tr')
|
||||
.should('have.length', verticalBorderIndices.length - 1)
|
||||
.each((row, rowIndex) => {
|
||||
cy.wrap(row)
|
||||
.find('.table-generator-cell')
|
||||
.should('have.length', horizontalBorderIndices.length - 1)
|
||||
.each((cell, cellIndex) => {
|
||||
if (cellIndex === 0) {
|
||||
cy.wrap(cell).should(
|
||||
horizontalBorderIndices[0] ? 'have.class' : 'not.have.class',
|
||||
'table-generator-cell-border-left'
|
||||
)
|
||||
}
|
||||
cy.wrap(cell).should(
|
||||
horizontalBorderIndices[cellIndex + 1]
|
||||
? 'have.class'
|
||||
: 'not.have.class',
|
||||
'table-generator-cell-border-right'
|
||||
)
|
||||
cy.wrap(cell).should(
|
||||
verticalBorderIndices[rowIndex] ? 'have.class' : 'not.have.class',
|
||||
'table-generator-row-border-top'
|
||||
)
|
||||
if (rowIndex === verticalBorderIndices.length - 2) {
|
||||
cy.wrap(cell).should(
|
||||
verticalBorderIndices[rowIndex + 1]
|
||||
? 'have.class'
|
||||
: 'not.have.class',
|
||||
'table-generator-row-border-bottom'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> Table editor', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptEvents()
|
||||
|
||||
cy.interceptMathJax()
|
||||
cy.interceptCompile({ prefix: 'compile', times: Number.MAX_SAFE_INTEGER })
|
||||
cy.intercept('/project/*/doc/*/metadata', { body: {} })
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
})
|
||||
|
||||
describe('Table rendering', function () {
|
||||
it('Renders a simple table', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{ccc}
|
||||
cell 1 & cell 2 & cell 3 \\\\
|
||||
cell 4 & cell 5 & cell 6 \\\\
|
||||
\\end{tabular}`)
|
||||
|
||||
// Find cell in table
|
||||
checkTable([
|
||||
['cell 1', 'cell 2', 'cell 3'],
|
||||
['cell 4', 'cell 5', 'cell 6'],
|
||||
])
|
||||
})
|
||||
|
||||
it('Renders a table with \\multicolumn', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{ccc}
|
||||
\\multicolumn{2}{c}{cell 1 and cell 2} & cell 3 \\\\
|
||||
cell 4 & cell 5 & cell 6 \\\\
|
||||
\\end{tabular}`)
|
||||
|
||||
// Find cell in table
|
||||
checkTable([
|
||||
[{ text: 'cell 1 and cell 2', colspan: 2 }, 'cell 3'],
|
||||
['cell 4', 'cell 5', 'cell 6'],
|
||||
])
|
||||
})
|
||||
|
||||
it('Renders borders', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c|c}
|
||||
cell 1 & cell 2 \\\\
|
||||
\\hline
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
|
||||
checkBordersWithNoMultiColumn([false, true, false], [false, true, false])
|
||||
})
|
||||
|
||||
it('Renders math in cells', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c}
|
||||
$\\pi$
|
||||
\\end{tabular}`)
|
||||
cy.get('.MathJax').should('have.text', '$\\pi$')
|
||||
})
|
||||
})
|
||||
|
||||
describe('The toolbar', function () {
|
||||
it('Renders the toolbar when the table is selected', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c}
|
||||
cell
|
||||
\\end{tabular}
|
||||
`)
|
||||
cy.get('.table-generator-floating-toolbar').should('not.exist')
|
||||
cy.get('.table-generator-cell').click()
|
||||
cy.get('.table-generator-floating-toolbar').should('exist')
|
||||
// The element is partially covered, but we can still click it
|
||||
cy.get('.cm-line').first().click({ force: true })
|
||||
cy.get('.table-generator-floating-toolbar').should('not.exist')
|
||||
})
|
||||
|
||||
it('Adds and removes borders when theme is changed', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c|c}
|
||||
cell 1 & cell 2 \\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}
|
||||
`)
|
||||
checkBordersWithNoMultiColumn([false, false, false], [false, true, false])
|
||||
cy.get('.table-generator-floating-toolbar').should('not.exist')
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar').findByText('Custom borders').click({ force: true })
|
||||
cy.get('.table-generator').findByText('All borders').click()
|
||||
// The element is partially covered, but we can still click it
|
||||
cy.get('.cm-line').first().click({ force: true })
|
||||
// Table should be unchanged
|
||||
checkTable([
|
||||
['cell 1', 'cell 2'],
|
||||
['cell 3', 'cell 4'],
|
||||
])
|
||||
checkBordersWithNoMultiColumn([true, true, true], [true, true, true])
|
||||
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('@toolbar').findByText('All borders').click()
|
||||
cy.get('.table-generator').findByText('No borders').click()
|
||||
// The element is partially covered, but we can still click it
|
||||
cy.get('.cm-line').first().click({ force: true })
|
||||
// Table should be unchanged
|
||||
checkTable([
|
||||
['cell 1', 'cell 2'],
|
||||
['cell 3', 'cell 4'],
|
||||
])
|
||||
checkBordersWithNoMultiColumn(
|
||||
[false, false, false],
|
||||
[false, false, false]
|
||||
)
|
||||
})
|
||||
|
||||
it('Changes the column alignment with dropdown buttons', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc}
|
||||
cell 1 & cell 2 \\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}
|
||||
`)
|
||||
|
||||
cy.get('.table-generator-cell')
|
||||
.should('have.class', 'alignment-center')
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Alignment')
|
||||
.should('be.disabled')
|
||||
.should('contain.text', 'format_align_center')
|
||||
|
||||
cy.get('.column-selector').first().click()
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Alignment')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
cy.get('.table-generator').findByLabelText('Left').click()
|
||||
// The element is partially covered, but we can still click it
|
||||
cy.get('.cm-line').first().click({ force: true })
|
||||
// Table contents shouldn't have changed
|
||||
checkTable([
|
||||
['cell 1', 'cell 2'],
|
||||
['cell 3', 'cell 4'],
|
||||
])
|
||||
// Check that alignment button updated to reflect the left alignment
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Alignment')
|
||||
.should('be.disabled')
|
||||
.should('contain.text', 'format_align_left')
|
||||
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(0)
|
||||
.should('have.class', 'alignment-left')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(1)
|
||||
.should('have.class', 'alignment-center')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(2)
|
||||
.should('have.class', 'alignment-left')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(3)
|
||||
.should('have.class', 'alignment-center')
|
||||
})
|
||||
|
||||
it('Removes rows and columns', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{|c|c|c|}
|
||||
\\hline
|
||||
cell 1 & cell 2 & cell 3 \\\\ \\hline
|
||||
cell 4 & cell 5 & cell 6 \\\\ \\hline
|
||||
cell 7 & cell 8 & cell 9 \\\\ \\hline
|
||||
\\end{tabular}
|
||||
`)
|
||||
checkTable([
|
||||
['cell 1', 'cell 2', 'cell 3'],
|
||||
['cell 4', 'cell 5', 'cell 6'],
|
||||
['cell 7', 'cell 8', 'cell 9'],
|
||||
])
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Delete row or column')
|
||||
.should('be.disabled')
|
||||
cy.get('.column-selector').eq(1).click()
|
||||
cy.get('@toolbar').findByLabelText('Delete row or column').click()
|
||||
checkTable([
|
||||
['cell 1', 'cell 3'],
|
||||
['cell 4', 'cell 6'],
|
||||
['cell 7', 'cell 9'],
|
||||
])
|
||||
cy.get('.row-selector').eq(2).click()
|
||||
cy.get('@toolbar').findByLabelText('Delete row or column').click()
|
||||
checkTable([
|
||||
['cell 1', 'cell 3'],
|
||||
['cell 4', 'cell 6'],
|
||||
])
|
||||
checkBordersWithNoMultiColumn([true, true, true], [true, true, true])
|
||||
})
|
||||
|
||||
it('Removes rows correctly when removing from the left', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{|c|c|c|}\\hline
|
||||
cell 1&cell 2&cell 3 \\\\\\hline
|
||||
\\end{tabular}
|
||||
`)
|
||||
checkTable([['cell 1', 'cell 2', 'cell 3']])
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('.table-generator')
|
||||
.findByText('cell 1')
|
||||
.type('{shift}{rightarrow}')
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Delete row or column')
|
||||
.should('be.enabled')
|
||||
cy.get('@toolbar').findByLabelText('Delete row or column').click()
|
||||
checkTable([['cell 3']])
|
||||
checkBordersWithNoMultiColumn([true, true], [true, true])
|
||||
})
|
||||
|
||||
it('Merges and unmerged cells', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{ccc}
|
||||
cell 1 & cell 2 & cell 3 \\\\
|
||||
cell 4 & cell 5 & cell 6 \\\\
|
||||
\\end{tabular}
|
||||
`)
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('.table-generator-cell').first().type('{shift}{rightarrow}')
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar').findByLabelText('Merge cells').click()
|
||||
checkTable([
|
||||
[{ text: 'cell 1 cell 2', colspan: 2 }, 'cell 3'],
|
||||
['cell 4', 'cell 5', 'cell 6'],
|
||||
])
|
||||
cy.get('@toolbar').findByLabelText('Unmerge cells').click()
|
||||
checkTable([
|
||||
['cell 1 cell 2', '', 'cell 3'],
|
||||
['cell 4', 'cell 5', 'cell 6'],
|
||||
])
|
||||
})
|
||||
|
||||
it('Adds rows and columns', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c}
|
||||
cell 1
|
||||
\\end{tabular}
|
||||
`)
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
|
||||
// Set border theme to "All borders" so that we can check that theme is
|
||||
// preserved when adding new rows and columns
|
||||
cy.get('@toolbar').findByText('No borders').click()
|
||||
cy.get('.table-generator').findByText('All borders').click()
|
||||
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||
cy.get('.table-generator').findByText('Insert column left').click()
|
||||
checkTable([['', 'cell 1']])
|
||||
checkBordersWithNoMultiColumn([true, true], [true, true, true])
|
||||
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||
cy.get('.table-generator').findByText('Insert column right').click()
|
||||
checkTable([['', 'cell 1', '']])
|
||||
checkBordersWithNoMultiColumn([true, true], [true, true, true, true])
|
||||
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||
cy.get('.table-generator').findByText('Insert row above').click()
|
||||
checkTable([
|
||||
['', '', ''],
|
||||
['', 'cell 1', ''],
|
||||
])
|
||||
checkBordersWithNoMultiColumn(
|
||||
[true, true, true],
|
||||
[true, true, true, true]
|
||||
)
|
||||
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('@toolbar').findByLabelText('Insert').click()
|
||||
cy.get('.table-generator').findByText('Insert row below').click()
|
||||
checkTable([
|
||||
['', '', ''],
|
||||
['', 'cell 1', ''],
|
||||
['', '', ''],
|
||||
])
|
||||
checkBordersWithNoMultiColumn(
|
||||
[true, true, true, true],
|
||||
[true, true, true, true]
|
||||
)
|
||||
})
|
||||
|
||||
it('Removes the table on toolbar button click', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{c}
|
||||
cell 1
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar').findByLabelText('Delete table').click()
|
||||
cy.get('.table-generator').should('not.exist')
|
||||
})
|
||||
|
||||
it('Moves the caption when using dropdown', function () {
|
||||
mountEditor(`
|
||||
\\begin{table}
|
||||
\\caption{Table caption}
|
||||
\\label{tab:table}
|
||||
\\begin{tabular}{c}
|
||||
cell 1
|
||||
\\end{tabular}
|
||||
\\end{table}`)
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar').findByText('Caption above').click()
|
||||
cy.get('.table-generator-toolbar-dropdown-menu')
|
||||
.findByText('Caption below')
|
||||
.click()
|
||||
// Check that caption is below table
|
||||
cy.get('.ol-cm-command-caption').then(([caption]) => {
|
||||
const { top: captionYPosition } = caption.getBoundingClientRect()
|
||||
cy.get('.table-generator').then(([table]) => {
|
||||
const { top: tableYPosition } = table.getBoundingClientRect()
|
||||
cy.wrap(captionYPosition).should('be.greaterThan', tableYPosition)
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('@toolbar').findByText('Caption below').click()
|
||||
cy.get('.table-generator-toolbar-dropdown-menu')
|
||||
.findByText('Caption above')
|
||||
.click()
|
||||
// Check that caption is above table
|
||||
cy.get('.ol-cm-command-caption').then(([caption]) => {
|
||||
const { top: captionYPosition } = caption.getBoundingClientRect()
|
||||
cy.get('.table-generator').then(([table]) => {
|
||||
const { top: tableYPosition } = table.getBoundingClientRect()
|
||||
cy.wrap(captionYPosition).should('be.lessThan', tableYPosition)
|
||||
})
|
||||
})
|
||||
|
||||
// Removes caption when clicking "No caption"
|
||||
cy.get('@toolbar').findByText('Caption above').click()
|
||||
cy.get('.table-generator-toolbar-dropdown-menu')
|
||||
.findByText('No caption')
|
||||
.click()
|
||||
cy.get('@toolbar').findByText('No caption').should('exist')
|
||||
cy.get('.ol-cm-command-caption').should('not.exist')
|
||||
cy.get('.ol-cm-command-label').should('not.exist')
|
||||
})
|
||||
|
||||
it('Renders a table with custom column spacing', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{@{}c@{}l!{}}
|
||||
cell 1 & cell 2 \\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
checkTable([
|
||||
['cell 1', 'cell 2'],
|
||||
['cell 3', 'cell 4'],
|
||||
])
|
||||
cy.get('.table-generator-cell').first().click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar').findByText('No borders').click()
|
||||
cy.get('.table-generator').findByText('All borders').click()
|
||||
// The element is partially covered, but we can still click it
|
||||
cy.get('.cm-line').first().click({ force: true })
|
||||
checkTable([
|
||||
['cell 1', 'cell 2'],
|
||||
['cell 3', 'cell 4'],
|
||||
])
|
||||
checkBordersWithNoMultiColumn([true, true, true], [true, true, true])
|
||||
})
|
||||
|
||||
it('Disables caption dropdown when not directly inside table environment', function () {
|
||||
mountEditor(`
|
||||
\\begin{table}
|
||||
\\caption{Table caption}
|
||||
\\label{tab:table}
|
||||
\\begin{adjustbox}{max width=\\textwidth}
|
||||
\\begin{tabular}{c}
|
||||
cell 1
|
||||
\\end{tabular}
|
||||
\\end{adjustbox}
|
||||
\\end{table}`)
|
||||
cy.get('.table-generator').findByText('cell 1').click()
|
||||
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
|
||||
cy.get('@toolbar')
|
||||
.contains('button', 'Caption above')
|
||||
.should('be.disabled')
|
||||
})
|
||||
|
||||
describe('Fixed width columns', function () {
|
||||
it('Can add fixed width columns', function () {
|
||||
// Check that no column indicators exist
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc}
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator-column-indicator-label').should('not.exist')
|
||||
cy.get('.table-generator-cell').eq(0).as('cell')
|
||||
// Activate the table
|
||||
cy.get('@cell').click()
|
||||
cy.get('.table-generator-floating-toolbar')
|
||||
.as('toolbar')
|
||||
.should('exist')
|
||||
// Select the second column
|
||||
cy.get('.column-selector').eq(1).click()
|
||||
cy.get('@toolbar').findByLabelText('Adjust column width').click()
|
||||
cy.get('.table-generator-toolbar-dropdown-menu')
|
||||
.findByText('Fixed width, wrap text')
|
||||
.click()
|
||||
// The modal should be open
|
||||
cy.get('.table-generator-width-modal').as('modal').should('be.visible')
|
||||
// The width input should be focused
|
||||
cy.get('@modal')
|
||||
.get('#column-width-modal-width')
|
||||
.should('be.focused')
|
||||
.type('20')
|
||||
// Change the unit to inches
|
||||
cy.get('@modal')
|
||||
.findAllByLabelText(/Length unit/)
|
||||
.first()
|
||||
.click()
|
||||
cy.get('@modal')
|
||||
.findByRole('listbox')
|
||||
.as('dropdown')
|
||||
.should('be.visible')
|
||||
cy.get('@dropdown').findByText('in').click()
|
||||
// Confirm the change
|
||||
cy.get('@modal').findByText('OK').click()
|
||||
// Modal should close
|
||||
cy.get('@modal').should('not.exist')
|
||||
// Check that the width is applied to the right column
|
||||
cy.get('.table-generator-column-widths-row td')
|
||||
// 3rd element (buffer, column 1, column 2)
|
||||
.eq(2)
|
||||
.should('contain.text', '20in')
|
||||
})
|
||||
|
||||
forEach([
|
||||
['20in', 'in'],
|
||||
['20', 'Custom'],
|
||||
['\\foobar', 'Custom'],
|
||||
]).it(
|
||||
`Understands '%s' width descriptor`,
|
||||
function (width, expectedUnit) {
|
||||
// Check that no column indicators exist
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cp{${width}}}
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
// Activate the table
|
||||
cy.get('.table-generator-cell').eq(0).click()
|
||||
// Click the column width indicator
|
||||
cy.get('.table-generator-column-indicator-label').click()
|
||||
// The modal should be open
|
||||
cy.get('.table-generator-width-modal')
|
||||
.as('modal')
|
||||
.should('be.visible')
|
||||
cy.get('@modal')
|
||||
.findAllByLabelText(/Length unit/)
|
||||
.first()
|
||||
.should('have.value', expectedUnit)
|
||||
cy.get('@modal').findByText('Cancel').click()
|
||||
}
|
||||
)
|
||||
|
||||
it(`It can justify fixed width cells`, function () {
|
||||
// Check that no column indicators exist
|
||||
mountEditor(`
|
||||
\\begin{tabular}{>{\\raggedright\\arraybackslash}p{2cm}c}
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
// Activate the table
|
||||
cy.get('.table-generator-cell').eq(0).click()
|
||||
cy.get('.table-generator-floating-toolbar')
|
||||
.as('toolbar')
|
||||
.should('exist')
|
||||
// Select the first column
|
||||
cy.get('.column-selector').first().click()
|
||||
// Verify current alignment is left, and open the menu
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Alignment')
|
||||
.should('not.be.disabled')
|
||||
.should('contain.text', 'format_align_left')
|
||||
.click()
|
||||
// Change to justified alignment
|
||||
cy.get('.table-generator').findByLabelText('Justify').click()
|
||||
// Verify that alignment icon and class alignments were updated
|
||||
cy.get('@toolbar')
|
||||
.findByLabelText('Alignment')
|
||||
.should('contain.text', 'format_align_justify')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(0)
|
||||
.should('have.class', 'alignment-paragraph')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(1)
|
||||
.should('have.class', 'alignment-center')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(2)
|
||||
.should('have.class', 'alignment-paragraph')
|
||||
cy.get('.table-generator-cell')
|
||||
.eq(3)
|
||||
.should('have.class', 'alignment-center')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tabular interactions', function () {
|
||||
it('Can type into cells', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cccc}
|
||||
cell 1 & cell 2 & cell 3 & cell 4\\\\
|
||||
cell 5 & \\multicolumn{2}{c}cell 6} & cell 7 \\\\
|
||||
\\end{tabular}`)
|
||||
|
||||
cy.get('.table-generator-cell').eq(0).as('cell-1')
|
||||
cy.get('.table-generator-cell').eq(5).as('cell-6')
|
||||
|
||||
// Escape should cancel editing
|
||||
cy.get('@cell-1').type('foo{Esc}')
|
||||
cy.get('@cell-1').should('have.text', 'cell 1')
|
||||
|
||||
// Enter should commit change. Direct typing should override the current contents
|
||||
cy.get('@cell-1').type('foo{Enter}')
|
||||
cy.get('@cell-1').should('have.text', 'foo')
|
||||
|
||||
// Enter should start editing at the end of current text
|
||||
cy.get('@cell-1').type('{Enter}')
|
||||
cy.get('@cell-1').find('textarea').should('exist')
|
||||
cy.get('@cell-1').type('bar{Enter}')
|
||||
cy.get('@cell-1').should('have.text', 'foobar')
|
||||
|
||||
// Double clicking should start editing at the end of current text
|
||||
cy.get('@cell-1').dblclick()
|
||||
cy.get('@cell-1').find('textarea').should('exist')
|
||||
cy.get('@cell-1').type('baz{Enter}')
|
||||
cy.get('@cell-1').should('have.text', 'foobarbaz')
|
||||
|
||||
cy.get('@cell-1').type('{Backspace}')
|
||||
cy.get('@cell-1').should('have.text', '')
|
||||
|
||||
// Typing also works for multicolumn cells
|
||||
cy.get('@cell-6').type('foo{Enter}')
|
||||
checkTable([
|
||||
['', 'cell 2', 'cell 3', 'cell 4'],
|
||||
['cell 5', { text: 'foo', colspan: 2 }, 'cell 7'],
|
||||
])
|
||||
})
|
||||
|
||||
it('Can paste tabular data into cells', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc }
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator-cell').eq(0).as('cell-1')
|
||||
// TODO: Seems as cypress can't access clipboard, so we can't test copying
|
||||
cy.get('@cell-1').click()
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/plain', 'foo\tbar\nbaz\tqux')
|
||||
cy.get('@cell-1').trigger('paste', { clipboardData })
|
||||
checkTable([
|
||||
['foo', 'bar'],
|
||||
['baz', 'qux'],
|
||||
])
|
||||
})
|
||||
|
||||
it('Can navigate cells with keyboard', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc }
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator-cell').eq(0).as('cell-1')
|
||||
cy.get('.table-generator-cell').eq(1).as('cell-2')
|
||||
cy.get('.table-generator-cell').eq(2).as('cell-3')
|
||||
cy.get('.table-generator-cell').eq(3).as('cell-4')
|
||||
|
||||
// Arrow key navigation
|
||||
cy.get('@cell-1').click()
|
||||
cy.get('@cell-1').type('{rightarrow}')
|
||||
cy.get('@cell-2').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-2').type('{leftarrow}')
|
||||
cy.get('@cell-1').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-1').type('{downarrow}')
|
||||
cy.get('@cell-3').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-3').type('{rightarrow}')
|
||||
cy.get('@cell-4').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-4').type('{uparrow}')
|
||||
cy.get('@cell-2').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-2').type('{leftarrow}')
|
||||
cy.get('@cell-1').should('have.focus').should('have.class', 'selected')
|
||||
|
||||
// Tab navigation
|
||||
cy.get('@cell-1').tab()
|
||||
cy.get('@cell-2').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-2').tab()
|
||||
cy.get('@cell-3').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-3').tab()
|
||||
cy.get('@cell-4').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-4').tab({ shift: true })
|
||||
cy.get('@cell-3').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-3').tab({ shift: true })
|
||||
cy.get('@cell-2').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-2').tab({ shift: true })
|
||||
cy.get('@cell-1').should('have.focus').should('have.class', 'selected')
|
||||
// Tabbing when editing a cell should commit change and move to next cell
|
||||
cy.get('@cell-1').type('foo')
|
||||
cy.get('@cell-1').tab()
|
||||
cy.get('@cell-2').should('have.focus').should('have.class', 'selected')
|
||||
cy.get('@cell-1').should('have.text', 'foo')
|
||||
})
|
||||
|
||||
it('Can select rows and columns with selectors', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc }
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator-cell').eq(0).as('cell-1')
|
||||
cy.get('.table-generator-cell').eq(1).as('cell-2')
|
||||
cy.get('.table-generator-cell').eq(2).as('cell-3')
|
||||
cy.get('.table-generator-cell').eq(3).as('cell-4')
|
||||
|
||||
cy.get('.column-selector').eq(0).click()
|
||||
cy.get('@cell-1').should('have.class', 'selected')
|
||||
cy.get('@cell-2').should('not.have.class', 'selected')
|
||||
cy.get('@cell-3').should('have.class', 'selected')
|
||||
cy.get('@cell-4').should('not.have.class', 'selected')
|
||||
cy.get('.column-selector').eq(1).click()
|
||||
cy.get('@cell-1').should('not.have.class', 'selected')
|
||||
cy.get('@cell-2').should('have.class', 'selected')
|
||||
cy.get('@cell-3').should('not.have.class', 'selected')
|
||||
cy.get('@cell-4').should('have.class', 'selected')
|
||||
cy.get('.column-selector').eq(0).click({ shiftKey: true })
|
||||
cy.get('@cell-1').should('have.class', 'selected')
|
||||
cy.get('@cell-2').should('have.class', 'selected')
|
||||
cy.get('@cell-3').should('have.class', 'selected')
|
||||
cy.get('@cell-4').should('have.class', 'selected')
|
||||
|
||||
cy.get('.row-selector').eq(0).click()
|
||||
cy.get('@cell-1').should('have.class', 'selected')
|
||||
cy.get('@cell-2').should('have.class', 'selected')
|
||||
cy.get('@cell-3').should('not.have.class', 'selected')
|
||||
cy.get('@cell-4').should('not.have.class', 'selected')
|
||||
cy.get('.row-selector').eq(1).click()
|
||||
cy.get('@cell-1').should('not.have.class', 'selected')
|
||||
cy.get('@cell-2').should('not.have.class', 'selected')
|
||||
cy.get('@cell-3').should('have.class', 'selected')
|
||||
cy.get('@cell-4').should('have.class', 'selected')
|
||||
cy.get('.row-selector').eq(0).click({ shiftKey: true })
|
||||
cy.get('@cell-1').should('have.class', 'selected')
|
||||
cy.get('@cell-2').should('have.class', 'selected')
|
||||
cy.get('@cell-3').should('have.class', 'selected')
|
||||
cy.get('@cell-4').should('have.class', 'selected')
|
||||
})
|
||||
|
||||
it('Allow compilation shortcuts to work', function () {
|
||||
mountEditor(`
|
||||
\\begin{tabular}{cc }
|
||||
cell 1 & cell 2\\\\
|
||||
cell 3 & cell 4 \\\\
|
||||
\\end{tabular}`)
|
||||
cy.get('.table-generator-cell').eq(0).as('cell-1').click()
|
||||
cy.get('@cell-1').type('{ctrl}{enter}')
|
||||
cy.wait('@compile')
|
||||
cy.get('@cell-1').type('foo{ctrl}{enter}')
|
||||
cy.wait('@compile')
|
||||
cy.get('@cell-1').type('{esc}')
|
||||
cy.get('@cell-1').type('{ctrl}{s}')
|
||||
cy.wait('@compile')
|
||||
cy.get('@cell-1').type('foo{ctrl}{s}')
|
||||
cy.wait('@compile')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,191 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> command tooltip in Visual mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
})
|
||||
|
||||
it('shows a tooltip for \\href', function () {
|
||||
const content = [
|
||||
'\\documentclass{article}',
|
||||
'\\usepackage{hyperref}',
|
||||
'\\begin{document}',
|
||||
'',
|
||||
'\\end{document}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'open').as('window-open')
|
||||
})
|
||||
|
||||
// wait for preamble to be escaped
|
||||
cy.get('.cm-line').eq(0).should('have.text', '')
|
||||
|
||||
// enter the command
|
||||
cy.get('.cm-line').eq(0).as('content-line')
|
||||
cy.get('@content-line').type('\\href{{}}{{}foo')
|
||||
cy.get('@content-line').should('have.text', '{foo}')
|
||||
|
||||
// enter the URL in the tooltip form
|
||||
cy.findByLabelText('URL').type('https://example.com')
|
||||
|
||||
// open the link
|
||||
cy.findByRole('button', { name: 'Go to page' }).click()
|
||||
cy.get('@window-open').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match.has('href', 'https://example.com/'),
|
||||
'_blank'
|
||||
)
|
||||
|
||||
// remove the link
|
||||
cy.findByRole('menu').should('have.length', 1)
|
||||
cy.findByRole('button', { name: 'Remove link' }).click()
|
||||
cy.findByRole('menu').should('have.length', 0)
|
||||
cy.get('@content-line').should('have.text', 'foo')
|
||||
})
|
||||
|
||||
it('can navigate the \\href tooltip using the keyboard', function () {
|
||||
const content = [
|
||||
'\\documentclass{article}',
|
||||
'\\usepackage{hyperref}',
|
||||
'\\begin{document}',
|
||||
'',
|
||||
'\\end{document}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// wait for preamble to be escaped
|
||||
cy.get('.cm-line').eq(0).should('have.text', '')
|
||||
|
||||
// enter the command
|
||||
cy.get('.cm-line').eq(0).as('content-line')
|
||||
cy.get('@content-line').type('\\href{{}}{{}foo')
|
||||
|
||||
// into tooltip URL form input
|
||||
cy.tab()
|
||||
// down to first button
|
||||
cy.tab()
|
||||
// back into tooltip URL form input
|
||||
cy.tab({ shift: true })
|
||||
// back into document
|
||||
cy.tab({ shift: true })
|
||||
|
||||
// close the tooltip
|
||||
cy.get('@content-line').trigger('keydown', { key: 'Escape' })
|
||||
cy.findByRole('menu').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('shows a tooltip for \\url', function () {
|
||||
mountEditor('')
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'open').as('window-open')
|
||||
})
|
||||
|
||||
// enter the command and URL
|
||||
cy.get('.cm-line').eq(0).as('content-line')
|
||||
cy.get('@content-line').type('\\url{{}https://example.com')
|
||||
cy.get('@content-line').should('have.text', '{https://example.com}')
|
||||
|
||||
// open the link
|
||||
cy.findByRole('button', { name: 'Go to page' }).click()
|
||||
cy.get('@window-open').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match.has('href', 'https://example.com/'),
|
||||
'_blank'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows a tooltip for \\include', function () {
|
||||
mountEditor('')
|
||||
|
||||
// enter the command and file name
|
||||
cy.get('.cm-line').eq(0).as('content-line')
|
||||
cy.get('@content-line').type('\\include{{}foo')
|
||||
|
||||
// assert the focused command is undecorated
|
||||
cy.get('@content-line').should('have.text', '\\include{foo}')
|
||||
|
||||
cy.contains('figures/foo.tex').click()
|
||||
cy.get('@content-line').should('have.text', '\\include{figures/foo}')
|
||||
cy.get('@content-line').type('{leftArrow}')
|
||||
|
||||
// assert the unfocused command has a menu
|
||||
cy.findByRole('menu').should('have.length', 1)
|
||||
cy.findByText('Edit file')
|
||||
|
||||
// assert the unfocused command is decorated
|
||||
cy.get('@content-line').type('{downArrow}')
|
||||
cy.findByRole('menu').should('have.length', 0)
|
||||
cy.get('@content-line').should('have.text', '\\include{figures/foo}')
|
||||
})
|
||||
|
||||
it('shows a tooltip for \\input', function () {
|
||||
mountEditor('')
|
||||
|
||||
// enter the command and file name
|
||||
cy.get('.cm-line').eq(0).as('content-line')
|
||||
cy.get('@content-line').type('\\input{{}foo')
|
||||
|
||||
// assert the focused command is undecorated
|
||||
cy.get('@content-line').should('have.text', '\\input{foo}')
|
||||
|
||||
cy.contains('figures/foo.tex').click()
|
||||
cy.get('@content-line').should('have.text', '\\input{figures/foo}')
|
||||
cy.get('@content-line').type('{leftArrow}')
|
||||
|
||||
// open the target
|
||||
cy.findByRole('menu').should('have.length', 1)
|
||||
cy.findByRole('button', { name: 'Edit file' })
|
||||
|
||||
// assert the unfocused command is decorated
|
||||
cy.get('@content-line').type('{downArrow}')
|
||||
cy.findByRole('menu').should('have.length', 0)
|
||||
cy.get('@content-line').should('have.text', '\\input{figures/foo}')
|
||||
})
|
||||
|
||||
it('shows a tooltip for \\ref', function () {
|
||||
const content = ['\\section{Foo} \\label{sec:foo}', ''].join('\n')
|
||||
|
||||
mountEditor(content)
|
||||
|
||||
// assert the unfocused label is decorated
|
||||
cy.get('.cm-line').eq(0).as('heading-line')
|
||||
cy.get('@heading-line').should('have.text', '{Foo} 🏷sec:foo')
|
||||
|
||||
// enter the command and cross-reference label
|
||||
cy.get('.cm-line').eq(1).as('content-line')
|
||||
cy.get('@content-line').type('\\ref{{}sec:foo')
|
||||
cy.get('@content-line').should('have.text', '🏷{sec:foo}')
|
||||
|
||||
// open the target
|
||||
cy.findByRole('menu').should('have.length', 1)
|
||||
cy.findByRole('button', { name: 'Go to target' }).click()
|
||||
cy.findByRole('menu').should('have.length', 0)
|
||||
|
||||
// assert the focused label is undecorated
|
||||
cy.get('@heading-line').should('have.text', 'Foo \\label{sec:foo}')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> floats', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('decorates a caption', function () {
|
||||
mountEditor('\n\\caption{Foo}\n')
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
|
||||
it('decorates a caption in a figure', function () {
|
||||
mountEditor('\\begin{figure}\n\\caption{Foo}\n\\end{figure}\n')
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
|
||||
it('decorates a caption in a table', function () {
|
||||
mountEditor('\\begin{table}\n\\caption{Foo}\n\\end{table}\n')
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
|
||||
it('decorates a starred caption', function () {
|
||||
mountEditor('\n\\caption*{Foo}\n')
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
|
||||
it('decorates a starred figure', function () {
|
||||
mountEditor('\\begin{figure*}\n\\caption{Foo}\n\\end{figure*}\n')
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
|
||||
it('decorates a starred table', function () {
|
||||
mountEditor('\\begin{table*}\n\\caption{Foo}\n\\end{table*}\n')
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.get('.cm-content').should('have.text', 'Foo')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,314 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('creates a nested list inside an unindented list', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item Test',
|
||||
'\\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// create a nested list
|
||||
cy.get('.cm-line')
|
||||
.eq(1)
|
||||
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
})
|
||||
|
||||
it('creates a nested list inside an indented list', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
' \\item Test',
|
||||
' \\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// create a nested list
|
||||
cy.get('.cm-line')
|
||||
.eq(1)
|
||||
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
})
|
||||
|
||||
it('creates a nested list on Tab at the start of an item', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item Test',
|
||||
'\\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// move to the start of the item and press Tab
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('{leftArrow}'.repeat(4))
|
||||
cy.get('@line').trigger('keydown', {
|
||||
key: 'Tab',
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
})
|
||||
|
||||
it('does not creates a nested list on Tab when not at the start of an item', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item Test',
|
||||
'\\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// focus a line (at the end of a list item) and press Tab
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('.cm-line').eq(1).trigger('keydown', {
|
||||
key: 'Tab',
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test '].join(''))
|
||||
})
|
||||
|
||||
it('removes a nested list on Shift-Tab', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item Test',
|
||||
'\\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// move to the start of the list item and press Tab
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('{leftArrow}'.repeat(4))
|
||||
cy.get('@line').trigger('keydown', {
|
||||
key: 'Tab',
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
|
||||
// focus the indented line and press Shift-Tab
|
||||
cy.get('.cm-line').eq(1).trigger('keydown', {
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
})
|
||||
|
||||
it('does not remove a top-level nested list on Shift-Tab', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item Test',
|
||||
'\\item Test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
// focus a list item and press Shift-Tab
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('.cm-line').eq(1).trigger('keydown', {
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
})
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||
})
|
||||
|
||||
it('handles up arrow at the start of a list item', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item One',
|
||||
'\\item Two',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
||||
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
||||
|
||||
cy.window().should(win => {
|
||||
expect(win.getSelection()?.toString()).to.equal('One')
|
||||
})
|
||||
})
|
||||
|
||||
it('handles up arrow at the start of an indented list item', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
' \\item One',
|
||||
' \\item Two',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(1).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
||||
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
||||
|
||||
cy.window().should(win => {
|
||||
expect(win.getSelection()?.toString()).to.equal('One')
|
||||
})
|
||||
})
|
||||
|
||||
it('handles keyboard navigation around a list', function () {
|
||||
const content = [
|
||||
'',
|
||||
'\\begin{itemize}',
|
||||
'\\item One',
|
||||
'\\item Two',
|
||||
'\\end{itemize}',
|
||||
'',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(0).as('line')
|
||||
cy.get('@line').click('left')
|
||||
cy.get('@line').type(
|
||||
'{downArrow}'.repeat(2) + // down to the second list item
|
||||
'{rightArrow}'.repeat(2) + // along a few characters
|
||||
'{upArrow}'.repeat(1) + // up to the first list item
|
||||
'{rightArrow}'.repeat(2) + // along to the start of the second list item
|
||||
'{shift}' + // start extending the selection
|
||||
'{rightArrow}'.repeat(3) // cover the word
|
||||
)
|
||||
|
||||
cy.window().should(win => {
|
||||
expect(win.getSelection()?.toString()).to.equal('Two')
|
||||
})
|
||||
})
|
||||
|
||||
it('positions the cursor after creating a new line with leading whitespace', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item foo bar',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
cy.get('.cm-line').eq(0).type('{leftArrow}'.repeat(4))
|
||||
cy.get('.cm-line').eq(0).type('{enter}baz')
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' foo', ' bazbar'].join(''))
|
||||
})
|
||||
|
||||
it('handles Enter in an empty list item at the end of a top-level list', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item foo',
|
||||
'\\item ',
|
||||
'\\end{itemize}',
|
||||
'',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.focused().type('{enter}')
|
||||
|
||||
cy.get('.cm-content').should('have.text', [' foo'].join(''))
|
||||
})
|
||||
|
||||
it('handles Enter in an empty list item at the end of a nested list', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item foo bar',
|
||||
'\\begin{itemize}',
|
||||
'\\item baz',
|
||||
'\\item ',
|
||||
'\\end{itemize}',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.focused().type('{enter}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[' foo', ' bar', ' baz', ' '].join('')
|
||||
)
|
||||
})
|
||||
|
||||
it('handles Enter in an empty list item at the end of a nested list with subsequent items', function () {
|
||||
const content = [
|
||||
'\\begin{itemize}',
|
||||
'\\item foo bar',
|
||||
'\\begin{itemize}',
|
||||
'\\item baz',
|
||||
'\\item ',
|
||||
'\\end{itemize}',
|
||||
'\\item test',
|
||||
'\\end{itemize}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(3).click()
|
||||
cy.focused().type('{enter}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[' foo', ' bar', ' baz', ' ', ' test'].join('')
|
||||
)
|
||||
})
|
||||
|
||||
it('decorates a description list', function () {
|
||||
const content = [
|
||||
'\\begin{description}',
|
||||
'\\item[foo] Bar',
|
||||
'\\item Test',
|
||||
'\\end{description}',
|
||||
].join('\n')
|
||||
mountEditor(content)
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
cy.get('.cm-content').should('have.text', ['foo Bar', 'Test'].join(''))
|
||||
|
||||
cy.get('.cm-line').eq(1).type('{Enter}baz')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
['foo Bar', 'Test', '[baz] '].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(2).type('{rightArrow}{rightArrow}Test')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
['foo Bar', 'Test', 'baz Test'].join('')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,802 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const menuIconsText = 'content_copyexpand_more'
|
||||
|
||||
const mountEditor = (content = '') => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').as('content')
|
||||
cy.get('@content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> paste HTML in Visual mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('handles paste', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.spy(clipboardData, 'getData').as('get-data')
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
cy.get('@content').type('{esc}')
|
||||
|
||||
cy.get('@content').should('have.text', 'foo' + menuIconsText)
|
||||
cy.get('@get-data').should('have.been.calledTwice')
|
||||
cy.get('@get-data').should('have.been.calledWithExactly', 'text/html')
|
||||
cy.get('@get-data').should('have.been.calledWithExactly', 'text/plain')
|
||||
})
|
||||
|
||||
it('handles a pasted bullet list', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = '<ul><li>foo</li><li>bar</li></ul>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', ' foo bar' + menuIconsText)
|
||||
cy.get('.ol-cm-item').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted numbered list', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = '<ol><li>foo</li><li>bar</li></ol>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', ' foo bar' + menuIconsText)
|
||||
cy.get('.ol-cm-item').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted nested bullet list', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<ul><li>foo</li><li><ul><li>bar</li><li>baz</li></ul></li></ul>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', ' foo bar baz' + menuIconsText)
|
||||
cy.get('.ol-cm-item').should('have.length', 4)
|
||||
cy.get('.cm-line').should('have.length', 6)
|
||||
})
|
||||
|
||||
it('handles a pasted nested numbered list', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<ol><li>foo</li><li><ol><li>bar</li><li>baz</li></ol></li></ol>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', ' foo bar baz' + menuIconsText)
|
||||
cy.get('.ol-cm-item').should('have.length', 4)
|
||||
cy.get('.cm-line').should('have.length', 6)
|
||||
})
|
||||
|
||||
it('removes a solitary item from a list', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = '<ul><li>foo</li></ul>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo' + menuIconsText)
|
||||
cy.get('.ol-cm-item').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('handles a pasted table', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr><td>foo</td><td>bar</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with cell borders', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 2)
|
||||
cy.get('.table-generator-cell-border-left').should('have.length', 1)
|
||||
cy.get('.table-generator-cell-border-right').should('have.length', 2)
|
||||
cy.get('.table-generator-row-border-top').should('have.length', 2)
|
||||
cy.get('.table-generator-row-border-bottom').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with row borders', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr style="border-top:1px solid black;border-bottom:1px solid black"><td>foo</td><td>bar</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 2)
|
||||
cy.get('.table-generator-cell-border-left').should('have.length', 0)
|
||||
cy.get('.table-generator-cell-border-right').should('have.length', 0)
|
||||
cy.get('.table-generator-row-border-top').should('have.length', 2)
|
||||
cy.get('.table-generator-row-border-bottom').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with adjacent borders', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
'<table><tbody>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr>',
|
||||
'</tbody></table>',
|
||||
].join('\n')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobarfoobarfoobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 6)
|
||||
cy.get('.table-generator-cell-border-left').should('have.length', 3)
|
||||
cy.get('.table-generator-cell-border-right').should('have.length', 6)
|
||||
cy.get('.table-generator-row-border-top').should('have.length', 6)
|
||||
cy.get('.table-generator-row-border-bottom').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with alignment', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr><td>foo</td><td style="text-align:left">foo</td><td style="text-align:center">foo</td><td style="text-align:right">foo</td><td style="text-align:justify">foo</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foofoofoofoofoo' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 5)
|
||||
cy.get('.table-generator-cell.alignment-left').should('have.length', 3)
|
||||
cy.get('.table-generator-cell.alignment-center').should('have.length', 1)
|
||||
cy.get('.table-generator-cell.alignment-right').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('handles a pasted table with merged columns', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
`<table><tbody>`,
|
||||
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
|
||||
`<tr><td colspan="2">test</td><td>test</td></tr>`,
|
||||
`<tr><td>test</td><td colspan="2" style="text-align:right">test</td></tr>`,
|
||||
`</tbody></table>`,
|
||||
].join('')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'testtesttesttesttesttesttest' + menuIconsText
|
||||
)
|
||||
cy.get('.table-generator-cell').should('have.length', 7)
|
||||
cy.get('.table-generator-cell[colspan="2"]').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with merged rows', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
`<table><tbody>`,
|
||||
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
|
||||
`<tr><td rowspan="2">test</td><td>test</td><td>test</td></tr>`,
|
||||
`<tr><td>test</td><td>test</td></tr>`,
|
||||
`</tbody></table>`,
|
||||
].join('')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'testtesttest\\multirow{2}{*}{test}testtesttesttest' + menuIconsText
|
||||
)
|
||||
cy.get('.table-generator-cell').should('have.length', 9)
|
||||
})
|
||||
|
||||
it('handles a pasted table with merged rows and columns', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
`<table><tbody>`,
|
||||
`<tr><td colspan="2" rowspan="2">test</td><td>test</td></tr>`,
|
||||
`<tr><td>test</td></tr>`,
|
||||
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
|
||||
`</tbody></table>`,
|
||||
].join('')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'\\multirow{2}{*}{test}testtesttesttesttest' + menuIconsText
|
||||
)
|
||||
cy.get('.table-generator-cell').should('have.length', 8)
|
||||
cy.get('.table-generator-cell[colspan="2"]').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('ignores rowspan="1" and colspan="1"', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
`<table><tbody>`,
|
||||
`<tr><td colspan="1" rowspan="1">test</td><td>test</td><td>test</td></tr>`,
|
||||
`<tr><td>test</td><td>test</td><td>test</td></tr>`,
|
||||
`</tbody></table>`,
|
||||
].join('')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'testtesttesttesttesttest' + menuIconsText
|
||||
)
|
||||
cy.get('.table-generator-cell').should('have.length', 6)
|
||||
cy.get('.table-generator-cell[colspan]').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('handles a pasted table with adjacent borders and merged cells', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
'<table><tbody>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black" colspan="2">foo</td></tr>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr>',
|
||||
'<tr><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">foo</td><td style="border-left:1px solid black;border-right:1px solid black;border-top:1px solid black;border-bottom:1px solid black">bar</td></tr>',
|
||||
'</tbody></table>',
|
||||
].join('\n')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foofoobarfoobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 5)
|
||||
cy.get('.table-generator-cell[colspan="2"]').should('have.length', 1)
|
||||
cy.get('.table-generator-cell-border-left').should('have.length', 2)
|
||||
cy.get('.table-generator-cell-border-right').should('have.length', 4)
|
||||
cy.get('.table-generator-row-border-top').should('have.length', 5)
|
||||
cy.get('.table-generator-row-border-bottom').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with cell styles', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr><td style="font-weight:bold">foo</td><td style="font-style:italic">bar</td><td style="font-style:italic;font-weight:bold">baz</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobarbaz' + menuIconsText)
|
||||
cy.findByText(/Sorry/).should('not.exist')
|
||||
cy.get('td b').should('have.length', 2)
|
||||
cy.get('td i').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles a pasted table with formatting markup', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><tbody><tr>' +
|
||||
'<td><b>foo</b></td>' +
|
||||
'<td><i>bar</i></td>' +
|
||||
'<td><b><i>baz</i></b></td>' +
|
||||
'<td><i><b>buzz</b></i></td>' +
|
||||
'<td><sup>up</sup></td>' +
|
||||
'<td><sub>down</sub></td>' +
|
||||
'</tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'foobarbazbuzzupdown' + menuIconsText
|
||||
)
|
||||
cy.findByText(/Sorry/).should('not.exist')
|
||||
cy.get('td b').should('have.length', 3)
|
||||
cy.get('td i').should('have.length', 3)
|
||||
cy.get('td sup').should('have.length', 1)
|
||||
cy.get('td sub').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('handles a pasted table with a caption', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<table><caption>A table</caption><tbody><tr><td>foo</td><td>bar</td></tr></tbody></table>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'A tablefoobar' + menuIconsText)
|
||||
cy.get('.table-generator-cell').should('have.length', 2)
|
||||
cy.get('.ol-cm-command-caption').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('handles a pasted link', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<a href="https://example.com/?q=$foo_~bar&x=\\bar#fragment{y}%2">foo</a>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', '{foo}' + menuIconsText)
|
||||
cy.get('.ol-cm-command-href').should('have.length', 1)
|
||||
|
||||
cy.get('.cm-line').eq(0).type('{leftArrow}')
|
||||
cy.findByLabelText('URL').should(
|
||||
'have.value',
|
||||
'https://example.com/?q=$foo_~bar&x=\\\\bar\\#fragment%7By%7D\\%2'
|
||||
)
|
||||
// TODO: assert that the "Go to page" link has been unescaped
|
||||
})
|
||||
|
||||
it('handles pasted code in pre blocks', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = `test <pre><code>\\textbf{foo}</code></pre> <pre style="font-family: 'Lucida Console', monospace">\\textbf{foo}</pre> test`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'test \\textbf{foo}\\textbf{foo}test' + menuIconsText
|
||||
)
|
||||
cy.get('.ol-cm-environment-verbatim').should('have.length', 10)
|
||||
})
|
||||
|
||||
it('handles a pasted blockquote', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'test <blockquote>foo</blockquote> test'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'test footest' + menuIconsText)
|
||||
cy.get('.ol-cm-environment-quote').should('have.length', 5)
|
||||
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'test \\begin{quote}foo\\end{quote}test' + menuIconsText
|
||||
)
|
||||
})
|
||||
|
||||
it('handles pasted paragraphs', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
'test',
|
||||
'<p>foo</p>',
|
||||
'<p>bar</p>',
|
||||
'<p>baz</p>',
|
||||
'test',
|
||||
].join('\n')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'test foobarbaztest' + menuIconsText)
|
||||
cy.get('.cm-line').should('have.length', 7)
|
||||
})
|
||||
|
||||
it('handles pasted paragraphs in list items and table cells', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
'test',
|
||||
'<p>foo</p><p>bar</p><p>baz</p>',
|
||||
'<ul><li><p>foo</p></li><li><p>foo</p></li></ul>',
|
||||
'<ol><li><p>foo</p></li><li><p>foo</p></li></ol>',
|
||||
'<table><tbody><tr><td><p>foo</p></td></tr></tbody></table>',
|
||||
'test',
|
||||
].join('\n')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'test foobarbaz foo foo foo foofootest' + menuIconsText
|
||||
)
|
||||
cy.get('.cm-line').should('have.length', 14)
|
||||
})
|
||||
|
||||
it('handles pasted inline code', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'test <code>foo</code> test'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'test foo test' + menuIconsText)
|
||||
cy.get('.ol-cm-command-verb')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', 'foo')
|
||||
})
|
||||
|
||||
it('use text/plain for a wrapper code element', function () {
|
||||
mountEditor()
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', '<code>foo</code>')
|
||||
clipboardData.setData('text/plain', 'foo')
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo')
|
||||
cy.get('.ol-cm-command-verb').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('use text/plain for a code element in a pre element', function () {
|
||||
mountEditor()
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', '<pre><code>foo</code></pre>')
|
||||
clipboardData.setData('text/plain', 'foo')
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo')
|
||||
cy.get('.ol-cm-command-verb').should('have.length', 0)
|
||||
cy.get('.ol-cm-environment-verbatim').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('use text/plain for a pre element with monospace font', function () {
|
||||
mountEditor()
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData(
|
||||
'text/html',
|
||||
'<pre style="font-family:Courier,monospace">foo</pre>'
|
||||
)
|
||||
clipboardData.setData('text/plain', 'foo')
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo')
|
||||
cy.get('.ol-cm-command-verb').should('have.length', 0)
|
||||
cy.get('.ol-cm-environment-verbatim').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('handles pasted text with formatting', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<b>foo</b><sup>th</sup> <i>bar</i><sub>2</sub> baz <em>woo</em> <strong>woo</strong> woo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'footh bar2 baz woo woo woo' + menuIconsText
|
||||
)
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 2)
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 2)
|
||||
cy.get('.ol-cm-command-textsuperscript').should('have.length', 1)
|
||||
cy.get('.ol-cm-command-textsubscript').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('handles pasted text with bold CSS formatting', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<span style="font-weight:bold">foo</span> <span style="font-weight:800">foo</span> foo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo foo foo' + menuIconsText)
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('handles pasted text with italic CSS formatting', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = '<span style="font-style:italic">foo</span> foo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo foo' + menuIconsText)
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('handles pasted text with non-bold CSS', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<strong style="font-weight:normal">foo</strong> <strong style="font-weight:200">foo</strong> foo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo foo foo' + menuIconsText)
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('handles pasted text with non-italic CSS', function () {
|
||||
mountEditor()
|
||||
|
||||
const data =
|
||||
'<em style="font-style:normal">foo</em> <i style="font-style:normal">foo</i> foo'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo foo foo' + menuIconsText)
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('handles pasted elements with duplicate CSS formatting', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = [
|
||||
'<strong style="font-weight:bold">foo</strong>',
|
||||
'<b style="font-weight:bold">foo</b>',
|
||||
'<em style="font-style:italic">foo</em>',
|
||||
'<i style="font-style:italic">foo</i>',
|
||||
'foo',
|
||||
].join(' ')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 2)
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('removes a non-breaking space when a text node contains no other content', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo<span>\xa0</span>bar'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo bar' + menuIconsText)
|
||||
})
|
||||
|
||||
it('does not remove a non-breaking space when a text node contains other content', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo\xa0bar'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo bar' + menuIconsText)
|
||||
})
|
||||
|
||||
it('removes all zero-width spaces', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo\u200bbar'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foobar' + menuIconsText)
|
||||
})
|
||||
|
||||
it('ignores HTML pasted from VS Code', function () {
|
||||
mountEditor()
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', '<b>foo</b>')
|
||||
clipboardData.setData('text/plain', 'foo')
|
||||
clipboardData.setData('application/vnd.code.copymetadata', 'test')
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo')
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('protects special characters', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo & bar~baz'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo & bar~baz' + menuIconsText)
|
||||
cy.get('.ol-cm-character').should('have.length', 2)
|
||||
})
|
||||
|
||||
it('does not protect special characters in code blocks', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = 'foo & bar~baz <code>\\textbf{foo}</code>'
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
'foo & bar~baz \\verb|\\textbf{foo}|' + menuIconsText
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(0).type('{Enter}')
|
||||
cy.get('@content').should('have.text', 'foo & bar~baz \\textbf{foo}')
|
||||
cy.get('.ol-cm-character').should('have.length', 2)
|
||||
cy.get('.ol-cm-command-verb').should('have.length', 1)
|
||||
})
|
||||
|
||||
// FIXME: need to assert on source code
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('tidies whitespace in pasted tables', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = `<table>
|
||||
<tr>
|
||||
<td>
|
||||
<p><b>test</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('.cm-line').should('have.length', 8)
|
||||
})
|
||||
|
||||
it('tidies whitespace in pasted lists', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = `<ul>
|
||||
<li> foo </li>
|
||||
<li>
|
||||
|
||||
<p>
|
||||
|
||||
<b>test</b></p>
|
||||
<p>test test test
|
||||
test test
|
||||
test test test</p>
|
||||
</li>
|
||||
</ul>`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('.cm-line').should('have.length', 6)
|
||||
cy.get('@content').should(
|
||||
'have.text',
|
||||
' foo testtest test test test test test test test' + menuIconsText
|
||||
)
|
||||
})
|
||||
|
||||
it('collapses whitespace in adjacent inline elements', function () {
|
||||
mountEditor()
|
||||
|
||||
const data = `<p><b> foo </b><span> test </span><i> bar </i> baz</p>`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', data)
|
||||
cy.get('@content').trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('@content').should('have.text', 'foo test bar baz' + menuIconsText)
|
||||
})
|
||||
|
||||
it('treats a pasted image as a figure even if there is HTML', function () {
|
||||
mountEditor()
|
||||
|
||||
cy.fixture<Uint8Array>('images/gradient.png').then(image => {
|
||||
const file = new File([image], 'gradient.png', { type: 'image/png' })
|
||||
const html = `<meta charset="utf-8"><img src="https://example.com/gradient.png" alt="gradient">`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', html)
|
||||
clipboardData.items.add(file)
|
||||
cy.get('.cm-content').trigger('paste', { clipboardData })
|
||||
|
||||
// figure modal paste handler should appear
|
||||
cy.findByText('Upload from computer').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not treat a pasted image as a figure if there is Office HTML', function () {
|
||||
mountEditor()
|
||||
|
||||
cy.fixture<Uint8Array>('images/gradient.png').then(image => {
|
||||
const file = new File([image], 'gradient.png', { type: 'image/png' })
|
||||
const html = `<meta charset="utf-8"><meta name="ProgId" content="MS.Word"><img src="https://example.com/gradient.png" alt="gradient">`
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', html)
|
||||
clipboardData.items.add(file)
|
||||
cy.get('.cm-content').trigger('paste', { clipboardData })
|
||||
|
||||
// paste options button should appear
|
||||
cy.findByLabelText('Paste options').should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { PermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
|
||||
const FileTreePathProvider: FC = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
dirname: cy.stub(),
|
||||
findEntityByPath: cy.stub(),
|
||||
pathInFolder: cy.stub(),
|
||||
previewByPath: cy
|
||||
.stub()
|
||||
.as('previewByPath')
|
||||
.returns({ url: '/images/frog.jpg', extension: 'jpg' }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
const PermissionsProvider: FC = ({ children }) => (
|
||||
<PermissionsContext.Provider
|
||||
value={{
|
||||
read: true,
|
||||
comment: true,
|
||||
resolveOwnComments: false,
|
||||
resolveAllComments: false,
|
||||
trackedWrite: false,
|
||||
write: false,
|
||||
admin: false,
|
||||
labelVersion: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
)
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.permissions.write = false
|
||||
scope.permissions.trackedWrite = false
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ FileTreePathProvider, PermissionsProvider }}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> in Visual mode with read-only permission', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptMathJax()
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
})
|
||||
|
||||
it('decorates footnote content', function () {
|
||||
mountEditor('Foo \\footnote{Bar.} ')
|
||||
|
||||
// select the footnote, so it expands
|
||||
cy.get('.ol-cm-footnote').click()
|
||||
|
||||
cy.get('.cm-line').eq(0).as('first-line')
|
||||
cy.get('@first-line').should('contain', 'Foo')
|
||||
cy.get('@first-line').should('contain', 'Bar')
|
||||
})
|
||||
|
||||
it('does not display the table toolbar', function () {
|
||||
mountEditor('\\begin{tabular}{c}\n cell\n\\end{tabular}')
|
||||
|
||||
cy.get('.table-generator-floating-toolbar').should('not.exist')
|
||||
cy.get('.table-generator-cell').click()
|
||||
cy.get('.table-generator-floating-toolbar').should('not.exist')
|
||||
})
|
||||
|
||||
it('does not enter a table cell on double-click', function () {
|
||||
mountEditor('\\begin{tabular}{c}\n cell\n\\end{tabular}\n\n')
|
||||
|
||||
cy.get('.table-generator-cell').dblclick()
|
||||
cy.get('.table-generator-cell').get('textarea').should('not.exist')
|
||||
})
|
||||
|
||||
it('does not enter a table cell on Enter', function () {
|
||||
mountEditor('\\begin{tabular}{c}\n cell\n\\end{tabular}')
|
||||
|
||||
cy.get('.table-generator-cell').trigger('keydown', { key: 'Enter' })
|
||||
cy.get('.table-generator-cell').get('textarea').should('not.exist')
|
||||
})
|
||||
|
||||
it('does not paste into a table cell', function () {
|
||||
mountEditor('\\begin{tabular}{c}\n cell\n\\end{tabular}\n\n')
|
||||
|
||||
cy.get('.cm-line').last().click()
|
||||
cy.get('.table-generator-cell-render').eq(0).click()
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/plain', 'bar')
|
||||
cy.get('.table-generator-cell-render')
|
||||
.eq(0)
|
||||
.trigger('paste', { clipboardData })
|
||||
|
||||
cy.get('.cm-content').should('have.text', 'cell')
|
||||
})
|
||||
|
||||
it('does not display the figure edit button', function () {
|
||||
cy.intercept('/images/frog.jpg', { fixture: 'images/gradient.png' })
|
||||
|
||||
mountEditor(
|
||||
`\\begin{figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.5\\linewidth]{frog.jpg}
|
||||
\\caption{My caption}
|
||||
\\label{fig:my-label}
|
||||
\\end{figure}`
|
||||
)
|
||||
|
||||
cy.get('img.ol-cm-graphics').should('have.length', 1)
|
||||
cy.findByRole('button', { name: 'Edit figure' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('does not display editing features in the href tooltip', function () {
|
||||
mountEditor('\\href{https://example.com/}{foo}\n\n')
|
||||
|
||||
// move the selection outside the link
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
|
||||
// put the selection inside the href command
|
||||
cy.findByText('foo').click()
|
||||
|
||||
cy.findByRole('button', { name: 'Go to page' })
|
||||
cy.findByLabelText('URL').should('be.disabled')
|
||||
cy.findByRole('button', { name: 'Remove link' }).should('not.exist')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,332 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
const selectAll = () => {
|
||||
cy.get('.cm-content').trigger(
|
||||
'keydown',
|
||||
isMac ? { key: 'a', metaKey: true } : { key: 'a', ctrlKey: true }
|
||||
)
|
||||
}
|
||||
|
||||
const clickToolbarButton = (name: string) => {
|
||||
cy.findByRole('button', { name }).click()
|
||||
cy.findByRole('button', { name }).trigger('mouseout')
|
||||
}
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
}
|
||||
|
||||
describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
})
|
||||
|
||||
it('should handle Undo and Redo', function () {
|
||||
mountEditor('')
|
||||
cy.get('.cm-line').eq(0).type('hi')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
clickToolbarButton('Undo')
|
||||
cy.get('.cm-content').should('have.text', '')
|
||||
clickToolbarButton('Redo')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
})
|
||||
|
||||
it('should handle section level changes', function () {
|
||||
mountEditor('hi')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
|
||||
clickToolbarButton('Choose section heading level')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('Subsection').click()
|
||||
})
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
cy.get('.ol-cm-command-subsection').should('have.length', 1)
|
||||
|
||||
clickToolbarButton('Choose section heading level')
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByText('Normal text').click()
|
||||
})
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
cy.get('.ol-cm-command-subsection').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('should toggle Bold and Italic', function () {
|
||||
mountEditor('hi')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
selectAll()
|
||||
|
||||
// bold
|
||||
clickToolbarButton('Format Bold')
|
||||
cy.get('.cm-content').should('have.text', '{hi}')
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 1)
|
||||
clickToolbarButton('Format Bold')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
cy.get('.ol-cm-command-textbf').should('have.length', 0)
|
||||
|
||||
// italic
|
||||
clickToolbarButton('Format Italic')
|
||||
cy.get('.cm-content').should('have.text', '{hi}')
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 1)
|
||||
clickToolbarButton('Format Italic')
|
||||
cy.get('.cm-content').should('have.text', 'hi')
|
||||
cy.get('.ol-cm-command-textit').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('should wrap content with inline math', function () {
|
||||
mountEditor('2+3=5')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('Insert Math')
|
||||
cy.findByRole('button', { name: 'Insert Inline Math' }).click()
|
||||
cy.get('.cm-content').should('have.text', '\\(2+3=5\\)')
|
||||
})
|
||||
|
||||
it('should wrap content with display math', function () {
|
||||
mountEditor('2+3=5')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('Insert Math')
|
||||
cy.findByRole('button', { name: 'Insert Display Math' }).click()
|
||||
cy.get('.cm-content').should('have.text', '\\[2+3=5\\]')
|
||||
})
|
||||
|
||||
it('should wrap content with a link', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('Insert Link')
|
||||
cy.get('.cm-content').should('have.text', '{test}')
|
||||
cy.findByLabelText('URL') // tooltip form
|
||||
})
|
||||
|
||||
it('should insert a bullet list', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Bullet List')
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
|
||||
cy.get('.cm-line').eq(0).type('ing')
|
||||
cy.get('.cm-line').eq(0).should('have.text', ' testing')
|
||||
})
|
||||
|
||||
it('should insert a numbered list', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
|
||||
cy.get('.cm-line').eq(0).type('ing')
|
||||
cy.get('.cm-line').eq(0).should('have.text', ' testing')
|
||||
})
|
||||
|
||||
it('should toggle between list types', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(0).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
clickToolbarButton('Bullet List')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{itemize}',
|
||||
' test',
|
||||
'\\end{itemize}',
|
||||
].join('')
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove a list', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(0).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-content').should('have.text', 'test')
|
||||
})
|
||||
|
||||
it('should not remove a parent list', function () {
|
||||
mountEditor('test\ntest')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
' test',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Increase Indent' }).click()
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
' test',
|
||||
' \\begin{enumerate}',
|
||||
' test',
|
||||
' \\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-line').eq(0).type('{upArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
' test',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
})
|
||||
|
||||
it('should not remove a nested list', function () {
|
||||
mountEditor('test\ntest')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
' test',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Increase Indent' }).click()
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
' test',
|
||||
' \\begin{enumerate}',
|
||||
' test',
|
||||
' \\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(0).click()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'test',
|
||||
' \\begin{enumerate}',
|
||||
' test',
|
||||
' \\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
})
|
||||
|
||||
it('should display the Toggle Symbol Palette button when available', function () {
|
||||
window.metaAttributesCache.set('ol-symbolPaletteAvailable', true)
|
||||
mountEditor('')
|
||||
clickToolbarButton('Toggle Symbol Palette')
|
||||
})
|
||||
|
||||
it('should not display the Toggle Symbol Palette button when not available', function () {
|
||||
window.metaAttributesCache.set('ol-symbolPaletteAvailable', false)
|
||||
mountEditor('')
|
||||
cy.findByLabelText('Toggle Symbol Palette').should('not.exist')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
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'
|
||||
|
||||
describe('<CodeMirrorEditor/> tooltips in Visual mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptMathJax()
|
||||
cy.interceptMetadata()
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope('\n\n\n')
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
|
||||
cy.get('.cm-line').eq(0).as('first-line')
|
||||
})
|
||||
|
||||
it('displays a tooltip for \\href commands', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\href{{}https://example.com}{{}foo}{leftArrow}'
|
||||
)
|
||||
cy.get('.cm-content').should('have.text', '{foo}')
|
||||
cy.get('.cm-tooltip').should('have.length', 1)
|
||||
cy.get('.cm-tooltip').within(() => {
|
||||
cy.findByLabelText('URL').should('have.value', 'https://example.com')
|
||||
cy.findByLabelText('URL').type('/foo')
|
||||
cy.findByLabelText('URL').should('have.value', 'https://example.com/foo')
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'open').as('open-window')
|
||||
})
|
||||
cy.findByRole('button', { name: 'Go to page' }).click()
|
||||
cy.get('@open-window').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match.has('href', 'https://example.com/foo'),
|
||||
'_blank'
|
||||
)
|
||||
cy.findByRole('button', { name: 'Remove link' }).click()
|
||||
})
|
||||
cy.get('.cm-content').should('have.text', 'foo')
|
||||
cy.get('.cm-tooltip').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('displays a tooltip for \\url commands', function () {
|
||||
cy.get('@first-line').type('\\url{{}https://example.com}{leftArrow}')
|
||||
cy.get('.cm-content').should('have.text', '{https://example.com}')
|
||||
cy.get('.cm-tooltip').should('have.length', 1)
|
||||
cy.get('.cm-tooltip').within(() => {
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'open').as('open-window')
|
||||
})
|
||||
cy.findByRole('button', { name: 'Go to page' }).click()
|
||||
cy.get('@open-window').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match.has('href', 'https://example.com/'),
|
||||
'_blank'
|
||||
)
|
||||
})
|
||||
cy.get('@first-line').type('{rightArrow}{rightArrow}')
|
||||
cy.get('.cm-content').should('have.text', 'https://example.com')
|
||||
cy.get('.cm-tooltip').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('displays a tooltip for \\ref commands', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\label{{}fig:frog}{Enter}\\ref{{}fig:frog}{leftArrow}'
|
||||
)
|
||||
cy.get('.cm-content').should('have.text', '🏷fig:frog🏷{fig:frog}')
|
||||
cy.get('.cm-tooltip').should('have.length', 1)
|
||||
cy.get('.cm-tooltip').within(() => {
|
||||
cy.findByRole('button', { name: 'Go to target' }).click()
|
||||
})
|
||||
cy.window().then(win => {
|
||||
expect(win.getSelection()?.toString()).to.equal('fig:frog')
|
||||
})
|
||||
})
|
||||
|
||||
it('displays a tooltip for \\include commands', function () {
|
||||
cy.get('@first-line').type('\\include{{}main}{leftArrow}')
|
||||
cy.get('.cm-content').should('have.text', '\\include{main}')
|
||||
cy.get('.cm-tooltip').should('have.length', 1)
|
||||
cy.get('.cm-tooltip').within(() => {
|
||||
cy.findByRole('button', { name: 'Edit file' }).click()
|
||||
// TODO: assert event fired with "main.tex" as the name?
|
||||
})
|
||||
cy.get('@first-line').type('{rightArrow}{rightArrow}')
|
||||
cy.get('.cm-content').should('have.text', '🔗main')
|
||||
cy.get('.cm-tooltip').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('displays a tooltip for \\input commands', function () {
|
||||
cy.get('@first-line').type('\\input{{}main}{leftArrow}')
|
||||
cy.get('.cm-content').should('have.text', '\\input{main}')
|
||||
cy.get('.cm-tooltip').should('have.length', 1)
|
||||
cy.get('.cm-tooltip').within(() => {
|
||||
cy.findByRole('button', { name: 'Edit file' }).click()
|
||||
// TODO: assert event fired with "main.tex" as the name?
|
||||
})
|
||||
cy.get('@first-line').type('{rightArrow}{rightArrow}')
|
||||
cy.get('.cm-content').should('have.text', '🔗main')
|
||||
cy.get('.cm-tooltip').should('have.length', 0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,735 @@
|
||||
// Needed since eslint gets confused by mocha-each
|
||||
/* eslint-disable mocha/prefer-arrow-callback */
|
||||
import { FC } from 'react'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import forEach from 'mocha-each'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
cy.interceptMetadata()
|
||||
cy.interceptMathJax()
|
||||
|
||||
// 3 blank lines
|
||||
const content = '\n'.repeat(3)
|
||||
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
const FileTreePathProvider: FC = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
dirname: cy.stub(),
|
||||
findEntityByPath: cy.stub(),
|
||||
pathInFolder: cy.stub(),
|
||||
previewByPath: cy
|
||||
.stub()
|
||||
.as('previewByPath')
|
||||
.callsFake(path => ({ url: path, extension: 'png' })),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// wait for the content to be parsed and revealed
|
||||
cy.get('.cm-content').should('have.css', 'opacity', '1')
|
||||
|
||||
cy.get('.cm-line').eq(0).as('first-line')
|
||||
cy.get('.cm-line').eq(1).as('second-line')
|
||||
cy.get('.cm-line').eq(2).as('third-line')
|
||||
cy.get('.cm-line').eq(3).as('fourth-line')
|
||||
cy.get('.ol-cm-toolbar [aria-label="Format Bold"]').as('toolbar-bold')
|
||||
|
||||
cy.get('@first-line').click()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache.clear()
|
||||
})
|
||||
|
||||
forEach(['LaTeX', 'TeX']).it('renders the %s logo', function (logo) {
|
||||
cy.get('@first-line').type(`\\${logo}{{}}{Enter}`)
|
||||
cy.get('@first-line').should('have.text', logo)
|
||||
})
|
||||
|
||||
it('renders \\dots', function () {
|
||||
cy.get('@first-line').type('\\dots{Esc}')
|
||||
cy.get('@first-line').should('have.text', '\\dots')
|
||||
cy.get('@first-line').type('{Enter}')
|
||||
cy.get('@first-line').should('have.text', '…')
|
||||
})
|
||||
|
||||
it('creates a new list item on Enter', function () {
|
||||
cy.get('@first-line').type('\\begin{{}itemize')
|
||||
|
||||
// select the first autocomplete item
|
||||
cy.findByRole('option').eq(0).click()
|
||||
|
||||
cy.get('@first-line')
|
||||
.should('have.text', ' ')
|
||||
.find('.ol-cm-item')
|
||||
.should('have.length', 1)
|
||||
|
||||
cy.get('@first-line').type('test{Enter}test')
|
||||
|
||||
cy.get('@first-line')
|
||||
.should('have.text', ' test')
|
||||
.find('.ol-cm-item')
|
||||
.should('have.length', 1)
|
||||
cy.get('@second-line')
|
||||
.should('have.text', ' test')
|
||||
.find('.ol-cm-item')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
|
||||
it('finishes a list on Enter in the last item if empty', function () {
|
||||
cy.get('@first-line').type('\\begin{{}itemize')
|
||||
|
||||
// select the first autocomplete item
|
||||
cy.findByRole('option').eq(0).click()
|
||||
|
||||
cy.get('@first-line').type('test{Enter}{Enter}')
|
||||
|
||||
cy.get('@first-line')
|
||||
.should('have.text', ' test')
|
||||
.find('.ol-cm-item')
|
||||
.should('have.length', 1)
|
||||
|
||||
cy.get('.cm-line').eq(1).should('have.text', '')
|
||||
})
|
||||
|
||||
it('does not finish a list on Enter in an earlier item if empty', function () {
|
||||
cy.get('@first-line').type('\\begin{{}itemize')
|
||||
|
||||
// select the first autocomplete item
|
||||
cy.findByRole('option').eq(0).click()
|
||||
|
||||
cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}')
|
||||
cy.get('.cm-content').should('have.text', ' testtest')
|
||||
})
|
||||
|
||||
forEach(['textbf', 'textit']).it('handles \\%s text', function (command) {
|
||||
cy.get('@first-line').type(`\\${command}{`)
|
||||
cy.get('@first-line').should('have.text', `{}`)
|
||||
cy.get('@first-line').type('{rightArrow} ')
|
||||
cy.get('@first-line').should('have.text', '{} ')
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}test text')
|
||||
cy.get('@first-line').should('have.text', '{test text}')
|
||||
cy.get('@first-line').type('{rightArrow} foo')
|
||||
cy.get('@first-line').should('have.text', 'test text foo') // no braces
|
||||
cy.get('@first-line').find(`.ol-cm-command-${command}`)
|
||||
})
|
||||
|
||||
forEach([
|
||||
'part',
|
||||
'chapter',
|
||||
'section',
|
||||
'subsection',
|
||||
'subsubsection',
|
||||
'paragraph',
|
||||
'subparagraph',
|
||||
]).it('handles \\%s sectioning command', function (command) {
|
||||
cy.get('@first-line').type(`\\${command}{`)
|
||||
cy.get('@first-line').should('have.text', `\\${command}{}`)
|
||||
cy.get('@first-line').type('{rightArrow} ')
|
||||
cy.get('@first-line').should('have.text', `\\${command}{} `)
|
||||
// Type a section heading
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}title')
|
||||
cy.get('@first-line').should('have.text', '{title}') // braces are visible as cursor is adjacent
|
||||
cy.get('@first-line').type('{leftArrow}')
|
||||
cy.get('@first-line').should('have.text', 'title') // braces are hidden as cursor is not adjacent
|
||||
cy.get('@first-line').type('{Enter}')
|
||||
cy.get('@first-line').should('have.text', 'title') // braces are hidden as cursor is on the next line
|
||||
cy.get('@first-line').find(`.ol-cm-heading.ol-cm-command-${command}`)
|
||||
})
|
||||
|
||||
forEach([
|
||||
'textsc',
|
||||
'texttt',
|
||||
'textmd',
|
||||
'textsf',
|
||||
'textsubscript',
|
||||
'textsuperscript',
|
||||
'sout',
|
||||
'emph',
|
||||
'underline',
|
||||
'url',
|
||||
'caption',
|
||||
]).it('handles \\%s text', function (command) {
|
||||
cy.get('@first-line').type(`\\${command}{`)
|
||||
cy.get('@first-line').should('have.text', `\\${command}{}`)
|
||||
cy.get('@first-line').type('{rightArrow} ')
|
||||
cy.get('@first-line').should('have.text', `\\${command}{} `)
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}test text{rightArrow} ')
|
||||
cy.get('@first-line').should('have.text', 'test text ')
|
||||
cy.get('@first-line').find(`.ol-cm-command-${command}`)
|
||||
})
|
||||
|
||||
it('handles \\verb text', function () {
|
||||
cy.get('@first-line').type(`\\verb|`)
|
||||
cy.get('@first-line').should('have.text', `\\verb|`)
|
||||
cy.get('@first-line').type('| ')
|
||||
cy.get('@first-line').should('have.text', `\\verb|| `)
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}test text{rightArrow} ')
|
||||
cy.get('@first-line').should('have.text', 'test text ')
|
||||
cy.get('@first-line').find(`.ol-cm-command-verb`)
|
||||
})
|
||||
|
||||
forEach([
|
||||
['label', '🏷'],
|
||||
['cite', '📚'],
|
||||
['include', '🔗'],
|
||||
]).it('handles \\%s commands', function (command, icon) {
|
||||
cy.get('@first-line').type(`\\${command}{} `)
|
||||
cy.get('@first-line').should('have.text', `\\${command}{} `)
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}key')
|
||||
cy.get('@first-line').should('have.text', `\\${command}{key}`)
|
||||
cy.get('@first-line').type('{rightArrow}')
|
||||
cy.get('@first-line').should('have.text', `\\${command}{key}`)
|
||||
cy.get('@first-line').type(' ')
|
||||
cy.get('@first-line').should('have.text', `${icon}key `)
|
||||
})
|
||||
|
||||
forEach([['ref', '🏷']]).it(
|
||||
'handles \\%s commands',
|
||||
function (command, icon) {
|
||||
cy.get('@first-line').type(`\\${command}{} `)
|
||||
cy.get('@first-line').should('have.text', `${icon} `)
|
||||
cy.get('@first-line').type('{Backspace}{leftArrow}key')
|
||||
cy.get('@first-line').should('have.text', `${icon}{key}`)
|
||||
cy.get('@first-line').type('{rightArrow}')
|
||||
cy.get('@first-line').should('have.text', `${icon}{key}`)
|
||||
cy.get('@first-line').type(' ')
|
||||
cy.get('@first-line').should('have.text', `${icon}key `)
|
||||
}
|
||||
)
|
||||
|
||||
it('handles \\href command', function () {
|
||||
cy.get('@first-line').type('\\href{{}https://overleaf.com} ')
|
||||
cy.get('@first-line').should('have.text', '\\href{https://overleaf.com} ')
|
||||
cy.get('@first-line').type('{Backspace}{{}{Del}Overleaf ')
|
||||
cy.get('@first-line').should(
|
||||
'have.text',
|
||||
'\\href{https://overleaf.com}{Overleaf '
|
||||
)
|
||||
cy.get('@first-line').type('{Backspace}} ')
|
||||
cy.get('@first-line').should('have.text', 'Overleaf ')
|
||||
cy.get('@first-line').find('.ol-cm-link-text')
|
||||
})
|
||||
|
||||
it('displays unknown commands unchanged', function () {
|
||||
cy.get('@first-line').type('\\foo[bar]{{}baz} ')
|
||||
cy.get('@first-line').should('have.text', '\\foo[bar]{baz} ')
|
||||
})
|
||||
|
||||
describe('Figure environments', function () {
|
||||
beforeEach(function () {
|
||||
cy.get('@first-line').type('\\begin{{}figure')
|
||||
cy.get('@first-line').type('{Enter}') // end with cursor in file path
|
||||
})
|
||||
|
||||
it('loads figures', function () {
|
||||
cy.get('@third-line').type('path/to/image')
|
||||
|
||||
cy.get('@third-line').should(
|
||||
'contain.text',
|
||||
' \\includegraphics[width=0.5\\linewidth]{path/to/image}'
|
||||
)
|
||||
|
||||
// move the cursor out of the figure
|
||||
cy.get('@third-line').type('{DownArrow}{DownArrow}{DownArrow}{DownArrow}')
|
||||
|
||||
// Should be removed from dom when line is hidden
|
||||
cy.get('.cm-content').should(
|
||||
'not.contain.text',
|
||||
'\\includegraphics[width=0.5\\linewidth]{path/to/image}'
|
||||
)
|
||||
|
||||
cy.get('img.ol-cm-graphics').should('have.attr', 'src', 'path/to/image')
|
||||
})
|
||||
|
||||
it('marks lines as figure environments', function () {
|
||||
// inside the figure
|
||||
cy.get('@second-line').should('have.class', 'ol-cm-environment-figure')
|
||||
// outside the figure
|
||||
cy.get('.cm-line')
|
||||
.eq(6)
|
||||
.should('not.have.class', 'ol-cm-environment-figure')
|
||||
})
|
||||
|
||||
it('marks environment has centered when it has \\centering command', function () {
|
||||
// inside the figure
|
||||
cy.get('@third-line').should('have.class', 'ol-cm-environment-centered')
|
||||
// outside the figure
|
||||
cy.get('.cm-line')
|
||||
.eq(6)
|
||||
.should('not.have.class', 'ol-cm-environment-centered')
|
||||
// the line containing \centering
|
||||
cy.get('@second-line')
|
||||
.should('have.text', ' \\centering')
|
||||
.should('have.class', 'ol-cm-environment-centered')
|
||||
cy.get('@second-line').type('{Backspace}')
|
||||
cy.get('@second-line')
|
||||
.should('have.text', ' \\centerin')
|
||||
.should('not.have.class', 'ol-cm-environment-centered')
|
||||
})
|
||||
})
|
||||
|
||||
describe('verbatim environments', function () {
|
||||
beforeEach(function () {
|
||||
cy.get('@first-line').type('\\begin{{}verbatim')
|
||||
cy.get('@first-line').type('{Enter}test') // end with cursor in content
|
||||
})
|
||||
|
||||
it('marks lines as verbatim environments', function () {
|
||||
// inside the environment
|
||||
cy.get('@second-line').should('have.class', 'ol-cm-environment-verbatim')
|
||||
|
||||
// outside the environment
|
||||
cy.get('.cm-line')
|
||||
.eq(4)
|
||||
.should('not.have.class', 'ol-cm-environment-verbatim')
|
||||
|
||||
// move the cursor out of the environment
|
||||
cy.get('.cm-line').eq(4).click()
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('lstlisting environments', function () {
|
||||
beforeEach(function () {
|
||||
cy.get('@first-line').type('\\begin{{}lstlisting')
|
||||
cy.get('@first-line').type('{Enter}test') // end with cursor in content
|
||||
})
|
||||
|
||||
it('marks lines as lstlisting environments', function () {
|
||||
// inside the environment
|
||||
cy.get('@second-line').should(
|
||||
'have.class',
|
||||
'ol-cm-environment-lstlisting'
|
||||
)
|
||||
|
||||
// outside the environment
|
||||
cy.get('.cm-line')
|
||||
.eq(4)
|
||||
.should('not.have.class', 'ol-cm-environment-lstlisting')
|
||||
|
||||
// move the cursor out of the environment
|
||||
cy.get('.cm-line').eq(4).click()
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Toolbar', function () {
|
||||
describe('Formatting buttons highlighting', function () {
|
||||
it('handles empty selections inside of bold', function () {
|
||||
cy.get('@first-line').type('\\textbf{{}test}{LeftArrow}') // \textbf{test|}
|
||||
cy.get('@toolbar-bold').should('have.class', 'active')
|
||||
cy.get('@first-line').type('{LeftArrow}') // \textbf{tes|t}
|
||||
cy.get('@toolbar-bold').should('have.class', 'active')
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(3)) // \textbf{|test}
|
||||
cy.get('@toolbar-bold').should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('handles empty selections outside bold', function () {
|
||||
cy.get('@first-line').type('\\textbf{{}test}')
|
||||
cy.get('@toolbar-bold').should('not.have.class', 'active')
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(6))
|
||||
cy.get('@toolbar-bold').should('not.have.class', 'active')
|
||||
})
|
||||
|
||||
it('handles range selections inside bold', function () {
|
||||
cy.get('@first-line').type('\\textbf{{}test}')
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(4))
|
||||
cy.get('@first-line').type('{Shift}{RightArrow}{RightArrow}')
|
||||
cy.get('@toolbar-bold').should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('handles range selections spanning bold', function () {
|
||||
cy.get('@first-line').type('\\textbf{{}test} outside')
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(10))
|
||||
cy.get('@first-line').type('{Shift}' + '{RightArrow}'.repeat(5))
|
||||
cy.get('@toolbar-bold').should('not.have.class', 'active')
|
||||
})
|
||||
|
||||
it('does not highlight bold when commands at selection ends are different', function () {
|
||||
cy.get('@first-line').type('\\textbf{{}first} \\textbf{{}second}')
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(12))
|
||||
cy.get('@first-line').type('{Shift}' + '{RightArrow}'.repeat(7))
|
||||
cy.get('@toolbar-bold').should('not.have.class', 'active')
|
||||
})
|
||||
|
||||
it('highlight when ends share common formatting ancestor', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\textbf{{}\\textit{{}first} \\textit{{}second}}'
|
||||
)
|
||||
cy.get('@first-line').type('{LeftArrow}'.repeat(13))
|
||||
cy.get('@first-line').type('{Shift}' + '{RightArrow}'.repeat(7))
|
||||
cy.get('@toolbar-bold').should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Beamer frames', function () {
|
||||
it('hides markup', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\begin{{}frame}{{}Slide\\\\title}{Enter}\\end{{}frame}{Enter}'
|
||||
)
|
||||
cy.get('.ol-cm-divider')
|
||||
cy.get('.ol-cm-frame-title')
|
||||
})
|
||||
it('typesets title', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\begin{{}frame}{{}Slide\\\\title}{Enter}\\end{{}frame}{Enter}'
|
||||
)
|
||||
cy.get('.ol-cm-frame-title').should('contain.html', 'Slide<br>title')
|
||||
})
|
||||
|
||||
it('typesets math in title', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\begin{{}frame}{{}Slide $\\pi$}{Enter}\\end{{}frame}{Enter}'
|
||||
)
|
||||
cy.get('.MathJax').should('contain.text', '$\\pi$')
|
||||
})
|
||||
|
||||
it('typesets subtitle', function () {
|
||||
cy.get('@first-line').type(
|
||||
'\\begin{{}frame}{{}Slide title}{{}Slide subtitle}{Enter}\\end{{}frame}{Enter}'
|
||||
)
|
||||
cy.get('.ol-cm-frame-subtitle').should('have.html', 'Slide subtitle')
|
||||
})
|
||||
})
|
||||
|
||||
it('typesets \\maketitle', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author}',
|
||||
'\\title{{}Document title\\\\with $\\pi$}',
|
||||
'\\begin{{}document}',
|
||||
'\\maketitle',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
|
||||
cy.get('.ol-cm-maketitle').should('have.class', 'MathJax')
|
||||
cy.get('.ol-cm-title').should('contain.html', 'Document title<br>with')
|
||||
cy.get('.ol-cm-author').should('have.text', 'Author')
|
||||
|
||||
cy.get('.ol-cm-preamble-widget').click()
|
||||
const deleteLine =
|
||||
'{command}{leftArrow}{shift}{command}{rightArrow}{backspace}'
|
||||
|
||||
// italic, bold and emph
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type(
|
||||
'\\title{{}formatted with \\textit{{}italic} \\textbf{{}bold} \\emph{{}emph}}'
|
||||
)
|
||||
cy.get('.ol-cm-title').should(
|
||||
'contain.html',
|
||||
'formatted with <i>italic</i> <b>bold</b> <em>emph</em>'
|
||||
)
|
||||
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type(
|
||||
'\\title{{}title\\\\ \\textbf{{}\\textit{{}\\emph{{}formated}}} \\textit{{}only italic}}'
|
||||
)
|
||||
cy.get('.ol-cm-title').should(
|
||||
'contain.html',
|
||||
'title<br> <b><i><em>formated</em></i></b> <i>only italic</i>'
|
||||
)
|
||||
|
||||
// texttt command
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type('\\title{{}title with \\texttt{{}command}}')
|
||||
cy.get('.ol-cm-title').should(
|
||||
'contain.html',
|
||||
'title with <span class="ol-cm-command-texttt">command</span>'
|
||||
)
|
||||
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type(
|
||||
'\\title{{}title with \\texttt{{}\\textbf{{}command}}}'
|
||||
)
|
||||
cy.get('.ol-cm-title').should(
|
||||
'contain.html',
|
||||
'title with <span class="ol-cm-command-texttt"><b>command</b></span>'
|
||||
)
|
||||
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type('\\title{{}Title with \\& ampersands}')
|
||||
cy.get('.ol-cm-title').should('contain.html', 'Title with & ampersands')
|
||||
|
||||
// unsupported command
|
||||
cy.get('@second-line').type(deleteLine)
|
||||
cy.get('@second-line').type('\\title{{}My \\LaTeX{{}} document}')
|
||||
cy.get('.ol-cm-title').should('contain.html', 'My \\LaTeX{} document')
|
||||
})
|
||||
|
||||
it('decorates footnotes', function () {
|
||||
cy.get('@first-line').type('Foo \\footnote{{}Bar.} ')
|
||||
cy.get('@first-line').should('contain', 'Foo')
|
||||
cy.get('@first-line').should('not.contain', 'Bar')
|
||||
cy.get('@first-line').type('{leftArrow}')
|
||||
cy.get('@first-line').should('have.text', 'Foo \\footnote{Bar.} ')
|
||||
})
|
||||
|
||||
it('should show document preamble', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author}',
|
||||
'\\title{{}Document title}',
|
||||
'\\begin{{}document}',
|
||||
'\\maketitle',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
cy.get('.ol-cm-preamble-widget').should('have.length', 1)
|
||||
cy.get('.ol-cm-preamble-widget').click()
|
||||
|
||||
cy.get('.ol-cm-preamble-line').eq(0).should('contain', '\\author{Author}')
|
||||
cy.get('.ol-cm-preamble-line')
|
||||
.eq(1)
|
||||
.should('contain', '\\title{Document title}')
|
||||
cy.get('.ol-cm-preamble-line').eq(2).should('contain', '\\begin{document}')
|
||||
cy.get('.ol-cm-preamble-line').eq(3).should('not.exist')
|
||||
})
|
||||
|
||||
it('should exclude maketitle from preamble extents if nested in another environment', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author}',
|
||||
'\\title{{}Document title}',
|
||||
'\\begin{{}document}',
|
||||
'\\begin{{}frame}{{}Foo}',
|
||||
'\\maketitle',
|
||||
'\\end{{}frame}',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
cy.get('.ol-cm-preamble-widget').should('have.length', 1)
|
||||
cy.get('.ol-cm-preamble-widget').click()
|
||||
|
||||
cy.get('.ol-cm-preamble-line').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('should show multiple authors', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author \\and Author2}',
|
||||
'\\author{{}Author3}',
|
||||
'\\title{{}Document title}',
|
||||
'\\begin{{}document}',
|
||||
'\\maketitle',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
cy.get('.ol-cm-preamble-widget').should('have.length', 1)
|
||||
cy.get('.ol-cm-preamble-widget').click()
|
||||
|
||||
cy.get('.ol-cm-authors').should('have.length', 1)
|
||||
cy.get('.ol-cm-authors .ol-cm-author').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('should update authors', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author \\and Author2}',
|
||||
'\\author{{}Author3}',
|
||||
'\\title{{}Document title}',
|
||||
'\\begin{{}document}',
|
||||
'\\maketitle',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
cy.get('.ol-cm-preamble-widget').should('have.length', 1)
|
||||
cy.get('.ol-cm-preamble-widget').click()
|
||||
|
||||
cy.get('.ol-cm-authors').should('have.length', 1)
|
||||
cy.get('.ol-cm-author').eq(0).should('contain', 'Author')
|
||||
cy.get('.ol-cm-author').eq(1).should('contain', 'Author2')
|
||||
cy.get('.ol-cm-author').eq(2).should('contain', 'Author3')
|
||||
|
||||
cy.get('.ol-cm-author').eq(0).click()
|
||||
cy.get('.ol-cm-preamble-line').eq(0).type('{leftarrow}{backspace}New')
|
||||
cy.get('.ol-cm-author').eq(1).should('contain', 'AuthorNew')
|
||||
|
||||
// update author without changing node from/to coordinates
|
||||
cy.get('.ol-cm-author').eq(0).click()
|
||||
cy.get('.ol-cm-preamble-line').eq(0).type('{leftarrow}{shift}{leftarrow}X')
|
||||
cy.get('.ol-cm-author').eq(1).should('contain', 'AuthorNeX')
|
||||
})
|
||||
|
||||
it('should ignore some commands in author', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\author{{}Author with \\corref{{}cor1} and \\fnref{{}label2} in the name}',
|
||||
'\\title{{}Document title}',
|
||||
'\\begin{{}document}',
|
||||
'\\maketitle',
|
||||
'\\end{{}document}',
|
||||
'',
|
||||
].join('{Enter}')
|
||||
)
|
||||
|
||||
cy.get('.ol-cm-authors').should('have.length', 1)
|
||||
cy.get('.ol-cm-author').should(
|
||||
'contain.html',
|
||||
'Author with and in the name'
|
||||
)
|
||||
})
|
||||
|
||||
describe('decorates color commands', function () {
|
||||
it('decorates textcolor', function () {
|
||||
cy.get('@first-line').type('\\textcolor{{}red}{{}foo}')
|
||||
cy.get('.ol-cm-textcolor')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', 'foo')
|
||||
.should('have.attr', 'style', 'color: rgb(255,0,0)')
|
||||
})
|
||||
|
||||
it('decorates colorbox', function () {
|
||||
cy.get('@first-line').type('\\colorbox{{}yellow}{{}foo}')
|
||||
cy.get('.ol-cm-colorbox')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', 'foo')
|
||||
.should('have.attr', 'style', 'background-color: rgb(255,255,0)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handling of special characters', function () {
|
||||
it('decorates a tilde with a non-breaking space', function () {
|
||||
cy.get('@first-line').type('Test~test')
|
||||
cy.get('@first-line').should('have.text', 'Test\xa0test')
|
||||
})
|
||||
|
||||
it('decorates a backslash-prefixed tilde with a tilde', function () {
|
||||
cy.get('@first-line').type('Test\\~test')
|
||||
cy.get('@first-line').should('have.text', 'Test~test')
|
||||
})
|
||||
|
||||
it('decorates a backslash-prefixed dollar sign with a dollar sign', function () {
|
||||
cy.get('@first-line').type('\\$5.00')
|
||||
cy.get('@first-line').should('have.text', '$5.00')
|
||||
cy.get('.ol-cm-character').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('decorates line breaks', function () {
|
||||
cy.get('@first-line').type('Test \\\\ test')
|
||||
cy.get('@second-line').click()
|
||||
cy.get('@first-line').should('have.text', 'Test ↩ test')
|
||||
})
|
||||
|
||||
it('decorates spacing commands', function () {
|
||||
cy.get('@first-line').type('\\thinspace')
|
||||
cy.get('@second-line').click()
|
||||
cy.get('@first-line')
|
||||
.find('.ol-cm-space')
|
||||
.should('have.attr', 'style', 'width: calc(0.166667em);')
|
||||
})
|
||||
|
||||
it('decorates spacing symbols', function () {
|
||||
cy.get('@first-line').type('\\,')
|
||||
cy.get('@second-line').click()
|
||||
cy.get('@first-line')
|
||||
.find('.ol-cm-space')
|
||||
.should('have.attr', 'style', 'width: calc(0.166667em);')
|
||||
})
|
||||
})
|
||||
|
||||
describe('decorates theorems', function () {
|
||||
it('decorates a proof environment', function () {
|
||||
cy.get('@first-line').type(
|
||||
['\\begin{{}proof}{Enter}', 'foo{Enter}', '\\end{{}proof}{Enter}'].join(
|
||||
''
|
||||
)
|
||||
)
|
||||
cy.get('.cm-content').should('have.text', 'Prooffoo')
|
||||
})
|
||||
|
||||
it('decorates a theorem environment', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\begin{{}theorem}{Enter}',
|
||||
'foo{Enter}',
|
||||
'\\end{{}theorem}{Enter}',
|
||||
].join('')
|
||||
)
|
||||
cy.get('.cm-content').should('have.text', 'Theoremfoo')
|
||||
})
|
||||
|
||||
it('decorates a theorem environment with a label', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\begin{{}theorem}[Bar]{Enter}',
|
||||
'foo{Enter}',
|
||||
'\\end{{}theorem}{Enter}',
|
||||
].join('')
|
||||
)
|
||||
cy.get('.cm-content').should('have.text', 'Theorem (Bar)foo')
|
||||
})
|
||||
|
||||
it('decorates a custom theorem environment with a label', function () {
|
||||
cy.get('@first-line').type(
|
||||
[
|
||||
'\\newtheorem{{}thm}{{}Foo}{Enter}',
|
||||
'\\begin{{}thm}[Bar]{Enter}',
|
||||
'foo{Enter}',
|
||||
'\\end{{}thm}{Enter}',
|
||||
].join('')
|
||||
)
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
['\\newtheorem{thm}{Foo}', 'Foo (Bar)foo'].join('')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
forEach(['quote', 'quotation', 'quoting', 'displayquote']).it(
|
||||
'renders a %s environment',
|
||||
function (environment) {
|
||||
cy.get('@first-line').type(`\\begin{{}${environment}`)
|
||||
cy.findAllByRole('listbox').should('have.length', 1)
|
||||
cy.findByRole('listbox').contains(`\\begin{${environment}}`).click()
|
||||
cy.get('@second-line').type('foo')
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[`\\begin{${environment}}`, ' foo', `\\end{${environment}}`].join('')
|
||||
)
|
||||
cy.get('.cm-line').eq(4).click()
|
||||
cy.get('.cm-content').should('have.text', ' foo')
|
||||
}
|
||||
)
|
||||
|
||||
it('invokes MathJax when math is written', function () {
|
||||
cy.get('@first-line').type('foo $\\pi$ bar')
|
||||
cy.get('@second-line').type(
|
||||
'foo \n\\[\\epsilon{rightArrow}{rightArrow}\nbar'
|
||||
)
|
||||
cy.get('.MathJax').first().should('have.text', '\\pi')
|
||||
cy.get('.MathJax').eq(1).should('have.text', '\\epsilon')
|
||||
})
|
||||
|
||||
// TODO: \input
|
||||
// TODO: Abstract
|
||||
})
|
||||
@@ -0,0 +1,597 @@
|
||||
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { metaKey } from '../helpers/meta-key'
|
||||
import { docId } from '../helpers/mock-doc'
|
||||
import { activeEditorLine } from '../helpers/active-editor-line'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import { OnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
import { FC } from 'react'
|
||||
|
||||
describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('deletes selected text on Backspace', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('@line').type('this is some text')
|
||||
cy.get('@line').should('have.text', 'this is some text')
|
||||
cy.get('@line').type('{shift}{leftArrow}{leftArrow}{leftArrow}{leftArrow}')
|
||||
cy.get('@line').type('{backspace}')
|
||||
cy.get('@line').should('have.text', 'this is some ')
|
||||
})
|
||||
|
||||
it('renders client-side lint annotations in the gutter', function () {
|
||||
const scope = mockScope()
|
||||
const userSettings = { syntaxValidation: true }
|
||||
|
||||
cy.clock()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.tick(1000)
|
||||
cy.clock().invoke('restore')
|
||||
|
||||
// TODO: aria role/label for gutter markers?
|
||||
cy.get('.cm-lint-marker-error').should('have.length', 2)
|
||||
cy.get('.cm-lint-marker-warning').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('renders annotations in the gutter', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
scope.pdf.logEntryAnnotations = {
|
||||
[docId]: [
|
||||
{
|
||||
row: 20,
|
||||
type: 'error',
|
||||
text: 'Another error',
|
||||
},
|
||||
{
|
||||
row: 19,
|
||||
type: 'error',
|
||||
text: 'An error',
|
||||
},
|
||||
{
|
||||
row: 20,
|
||||
type: 'warning',
|
||||
text: 'A warning on the same line',
|
||||
},
|
||||
{
|
||||
row: 25,
|
||||
type: 'warning',
|
||||
text: 'Another warning',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const userSettings = { syntaxValidation: false }
|
||||
|
||||
cy.clock()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.tick(1000)
|
||||
cy.clock().invoke('restore')
|
||||
|
||||
// TODO: aria role/label for gutter markers?
|
||||
cy.get('.cm-lint-marker-error').should('have.length', 2)
|
||||
cy.get('.cm-lint-marker-warning').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('renders code in an editor', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.contains('Your introduction goes here!')
|
||||
})
|
||||
|
||||
it('does not indent when entering new line off non-empty line', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type('foo{enter}')
|
||||
|
||||
activeEditorLine().should('have.text', '')
|
||||
})
|
||||
|
||||
it('indents automatically when using snippet', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('@line').type('\\begin{{}itemiz')
|
||||
cy.findAllByRole('listbox').contains('\\begin{itemize}').click()
|
||||
|
||||
activeEditorLine().invoke('text').should('match', /^ {4}/)
|
||||
})
|
||||
|
||||
it('keeps indentation when going to a new line', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
// Single indentation
|
||||
cy.get('@line').trigger('keydown', { key: 'Tab' })
|
||||
cy.get('@line').type('{enter}')
|
||||
|
||||
activeEditorLine().should('have.text', ' ')
|
||||
|
||||
// Double indentation
|
||||
activeEditorLine().trigger('keydown', { key: 'Tab' }).type('{enter}')
|
||||
|
||||
activeEditorLine().should('have.text', ' ')
|
||||
})
|
||||
|
||||
it('renders cursor highlights', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
const value = {
|
||||
onlineUsers: {},
|
||||
onlineUserCursorHighlights: {
|
||||
[docId]: [
|
||||
{
|
||||
label: 'Test User',
|
||||
cursor: { row: 10, column: 5 },
|
||||
hue: 150,
|
||||
},
|
||||
{
|
||||
label: 'Another User',
|
||||
cursor: { row: 7, column: 2 },
|
||||
hue: 50,
|
||||
},
|
||||
{
|
||||
label: 'Starter User',
|
||||
cursor: { row: 0, column: 0 },
|
||||
hue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
onlineUsersArray: [],
|
||||
onlineUsersCount: 3,
|
||||
}
|
||||
|
||||
const OnlineUsersProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<OnlineUsersContext.Provider value={value}>
|
||||
{children}
|
||||
</OnlineUsersContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ OnlineUsersProvider }}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.ol-cm-cursorHighlight').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('does not allow typing to the document in read-only mode', function () {
|
||||
const scope = mockScope()
|
||||
scope.permissionsLevel = 'readOnly'
|
||||
scope.permissions.write = false
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Handling the thrown error on failing to type text
|
||||
cy.on('fail', error => {
|
||||
if (error.message.includes('it requires a valid typeable element')) {
|
||||
return
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('@line').type('text')
|
||||
cy.get('@line').should('not.contain.text', 'text')
|
||||
})
|
||||
|
||||
it('highlights matching brackets', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).click()
|
||||
|
||||
const pairs = ['()', '[]', '{}']
|
||||
|
||||
pairs.forEach(pair => {
|
||||
activeEditorLine().type(pair).as('line')
|
||||
cy.get('@line').find('.cm-matchingBracket')
|
||||
cy.get('@line').type('{enter}')
|
||||
})
|
||||
})
|
||||
|
||||
it('folds code', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// select foldable line
|
||||
cy.get('.cm-line').eq(9).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
const testUnfoldedState = () => {
|
||||
cy.get('.cm-gutterElement').eq(11).should('have.text', '11')
|
||||
|
||||
cy.get('.cm-gutterElement').eq(12).should('have.text', '12')
|
||||
}
|
||||
|
||||
const testFoldedState = () => {
|
||||
cy.get('.cm-gutterElement').eq(11).should('have.text', '13')
|
||||
|
||||
cy.get('.cm-gutterElement').eq(12).should('have.text', '14')
|
||||
}
|
||||
|
||||
testUnfoldedState()
|
||||
|
||||
// Fold
|
||||
cy.get('span[title="Fold line"]').eq(1).click()
|
||||
|
||||
testFoldedState()
|
||||
|
||||
// Unfold
|
||||
cy.get('span[title="Unfold line"]').eq(1).click()
|
||||
|
||||
testUnfoldedState()
|
||||
})
|
||||
|
||||
it('save file with `:w` command in vim mode', function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
const userSettings = { mode: 'vim' }
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Compile on initial load
|
||||
cy.waitForCompile()
|
||||
cy.interceptCompile()
|
||||
|
||||
// put the cursor on a blank line to type in
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
|
||||
cy.get('.cm-vim-panel').should('have.length', 0)
|
||||
|
||||
cy.get('@line').type(':')
|
||||
|
||||
cy.get('.cm-vim-panel').should('have.length', 1)
|
||||
|
||||
cy.get('.cm-vim-panel input').type('w')
|
||||
cy.get('.cm-vim-panel input').type('{enter}')
|
||||
|
||||
// Compile after save
|
||||
cy.waitForCompile()
|
||||
})
|
||||
|
||||
it('search and replace text', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(
|
||||
'{enter}text_to_find{enter}abcde 1{enter}abcde 2{enter}abcde 3{enter}ABCDE 4{enter}'
|
||||
)
|
||||
|
||||
// select text `text_to_find`
|
||||
cy.get('.cm-line').eq(17).as('lineToFind')
|
||||
cy.get('@lineToFind').dblclick()
|
||||
|
||||
// search panel is not displayed
|
||||
cy.findByRole('search').should('have.length', 0)
|
||||
|
||||
cy.get('@lineToFind').type(`{${metaKey}+f}`)
|
||||
|
||||
// search panel is displayed
|
||||
cy.findByRole('search').should('have.length', 1)
|
||||
|
||||
cy.findByRole('textbox', { name: 'Find' }).as('search-input')
|
||||
cy.findByRole('textbox', { name: 'Replace' }).as('replace-input')
|
||||
|
||||
cy.get('@search-input')
|
||||
// search input should be focused
|
||||
.should('be.focused')
|
||||
// search input's value should be set to the selected text
|
||||
.should('have.value', 'text_to_find')
|
||||
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@search-input').type('abcde')
|
||||
|
||||
cy.findByRole('button', { name: 'next' }).as('next-btn')
|
||||
cy.findByRole('button', { name: 'previous' }).as('previous-btn')
|
||||
|
||||
// shows the number of matches
|
||||
cy.contains(`1 of 4`)
|
||||
|
||||
for (let i = 4; i; i--) {
|
||||
// go to previous occurrence
|
||||
cy.get('@previous-btn').click()
|
||||
|
||||
// shows the number of matches
|
||||
cy.contains(`${i} of 4`)
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
// shows the number of matches
|
||||
cy.contains(`${i} of 4`)
|
||||
|
||||
// go to next occurrence
|
||||
cy.get('@next-btn').click()
|
||||
}
|
||||
|
||||
// roll round to 1
|
||||
cy.contains(`1 of 4`)
|
||||
|
||||
// matches case
|
||||
cy.contains('Aa').click()
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@search-input').type('ABCDE')
|
||||
cy.get('.cm-searchMatch-selected').should('contain.text', 'ABCDE')
|
||||
cy.get('@search-input').clear()
|
||||
cy.contains('Aa').click()
|
||||
|
||||
// matches regex
|
||||
cy.contains('[.*]').click()
|
||||
cy.get('@search-input').type('\\\\author\\{{}\\w+\\}')
|
||||
cy.get('.cm-searchMatch-selected').should('contain.text', '\\author{You}')
|
||||
cy.contains('[.*]').click()
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('.cm-searchMatch-selected').should('not.exist')
|
||||
|
||||
// replace
|
||||
cy.get('@search-input').type('abcde 1')
|
||||
cy.get('@replace-input').type('test 1')
|
||||
cy.findByRole('button', { name: 'Replace' }).click()
|
||||
cy.get('.cm-line')
|
||||
.eq(18)
|
||||
.should('contain.text', 'test 1')
|
||||
.should('not.contain.text', 'abcde')
|
||||
|
||||
// replace all
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@search-input').type('abcde')
|
||||
cy.get('@replace-input').clear()
|
||||
cy.get('@replace-input').type('test')
|
||||
cy.findByRole('button', { name: /replace all/i }).click()
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@replace-input').clear()
|
||||
cy.should('not.contain.text', 'abcde')
|
||||
|
||||
// replace all within selection
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@search-input').type('contentLine')
|
||||
cy.get('.ol-cm-search-form-position').should('have.text', '1 of 100')
|
||||
|
||||
cy.get('.cm-line').eq(27).as('contentLine')
|
||||
cy.get('@contentLine').should('contain.text', 'contentLine 0')
|
||||
cy.get('@contentLine').click()
|
||||
cy.get('@contentLine').type('{shift}{downArrow}{downArrow}{downArrow}')
|
||||
|
||||
cy.findByLabelText('Within selection').click()
|
||||
cy.get('.ol-cm-search-form-position').should('have.text', '1 of 3')
|
||||
cy.get('@replace-input').clear()
|
||||
cy.get('@replace-input').type('contentedLine')
|
||||
cy.findByRole('button', { name: /replace all/i }).click()
|
||||
cy.get('.cm-line:contains("contentedLine")').should('have.length', 3)
|
||||
cy.findByLabelText('Within selection').click()
|
||||
cy.get('.ol-cm-search-form-position').should('have.text', '2 of 97')
|
||||
cy.get('@search-input').clear()
|
||||
cy.get('@replace-input').clear()
|
||||
|
||||
// close the search form, to clear the stored query
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
})
|
||||
|
||||
it('navigates in the search panel', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
// Open the search panel
|
||||
cy.get('.cm-line').eq(16).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(`{${metaKey}+f}`)
|
||||
|
||||
cy.findByRole('search').within(() => {
|
||||
cy.findByLabelText('Find').as('find-input')
|
||||
cy.findByLabelText('Replace').as('replace-input')
|
||||
cy.get('[type="checkbox"][name="caseSensitive"]').as('case-sensitive')
|
||||
cy.get('[type="checkbox"][name="regexp"]').as('regexp')
|
||||
cy.get('[type="checkbox"][name="wholeWord"]').as('whole-word')
|
||||
cy.get('[type="checkbox"][name="withinSelection"]').as('within-selection')
|
||||
cy.get('label').contains('Aa').as('case-sensitive-label')
|
||||
cy.get('label').contains('[.*]').as('regexp-label')
|
||||
cy.get('label').contains('W').as('whole-word-label')
|
||||
cy.findByLabelText('Within selection').as('within-selection-label')
|
||||
cy.findByRole('button', { name: 'Replace' }).as('replace')
|
||||
cy.findByRole('button', { name: 'Replace All' }).as('replace-all')
|
||||
cy.findByRole('button', { name: 'previous' }).as('find-previous')
|
||||
cy.findByRole('button', { name: 'next' }).as('find-next')
|
||||
cy.findByRole('button', { name: 'Close' }).as('close')
|
||||
|
||||
// Tab forwards...
|
||||
cy.get('@find-input').should('be.focused').tab()
|
||||
cy.get('@replace-input').should('be.focused').tab()
|
||||
cy.get('@case-sensitive').should('be.focused').tab()
|
||||
cy.get('@regexp').should('be.focused').tab()
|
||||
cy.get('@whole-word').should('be.focused').tab()
|
||||
cy.get('@within-selection').should('be.focused').tab()
|
||||
cy.get('@find-previous').should('be.focused').tab()
|
||||
cy.get('@find-next').should('be.focused').tab()
|
||||
cy.get('@replace').should('be.focused').tab()
|
||||
cy.get('@replace-all').should('be.focused').tab()
|
||||
|
||||
// ... then backwards
|
||||
cy.get('@close').should('be.focused').tab({ shift: true })
|
||||
cy.get('@replace-all').should('be.focused').tab({ shift: true })
|
||||
cy.get('@replace').should('be.focused').tab({ shift: true })
|
||||
cy.get('@find-next').should('be.focused').tab({ shift: true })
|
||||
cy.get('@find-previous').should('be.focused').tab({ shift: true })
|
||||
cy.get('@within-selection').should('be.focused').tab({ shift: true })
|
||||
cy.get('@whole-word').should('be.focused').tab({ shift: true })
|
||||
cy.get('@regexp').should('be.focused').tab({ shift: true })
|
||||
cy.get('@case-sensitive').should('be.focused').tab({ shift: true })
|
||||
cy.get('@replace-input').should('be.focused').tab({ shift: true })
|
||||
cy.get('@find-input').should('be.focused')
|
||||
|
||||
for (const option of [
|
||||
'@case-sensitive-label',
|
||||
'@regexp-label',
|
||||
'@whole-word-label',
|
||||
'@within-selection-label',
|
||||
]) {
|
||||
// Toggle when clicked, then focus the search input
|
||||
cy.get(option).click()
|
||||
cy.get(option).should('have.class', 'checked')
|
||||
cy.get('@find-input').should('be.focused')
|
||||
|
||||
// Toggle when clicked again, then focus the search input
|
||||
cy.get(option).click()
|
||||
cy.get(option).should('not.have.class', 'checked')
|
||||
cy.get('@find-input').should('be.focused')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('restores stored cursor and scroll position', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
customLocalStorage.setItem(`doc.position.${docId}`, {
|
||||
cursorPosition: { row: 50, column: 5 },
|
||||
firstVisibleLine: 45,
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
activeEditorLine()
|
||||
.should('have.text', 'contentLine 29')
|
||||
.should(() => {
|
||||
const selection = window.getSelection() as Selection
|
||||
expect(selection.isCollapsed).to.be.true
|
||||
|
||||
const rect = selection.getRangeAt(0).getBoundingClientRect()
|
||||
expect(Math.round(rect.top)).to.be.gte(100)
|
||||
expect(Math.round(rect.left)).to.be.gte(90)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user