From e7386e6fe35993fab49b288197a10c8061bc1989 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Fri, 30 Dec 2022 09:25:31 -0800 Subject: [PATCH] Chore: Mobile: Fix CodeMirror test failures (#7522) --- .eslintignore | 3 + .gitignore | 3 + .../NoteEditor/CodeMirror/CodeMirror.test.ts | 2 + ...rkdownCommands.bulletedVsChecklist.test.ts | 19 +- .../CodeMirror/markdownCommands.test.ts | 166 +++++++----------- .../markdownCommands.toggleList.test.ts | 134 +++++++------- .../CodeMirror/markdownMathParser.test.ts | 95 +++++----- .../CodeMirror/testUtil/createEditor.ts | 46 ++++- .../CodeMirror/testUtil/loadLanguages.ts | 12 ++ packages/app-mobile/package.json | 4 +- yarn.lock | 61 ++++--- 11 files changed, 293 insertions(+), 252 deletions(-) create mode 100644 packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.ts diff --git a/.eslintignore b/.eslintignore index 6642ce56c..e64e5ecea 100644 --- a/.eslintignore +++ b/.eslintignore @@ -933,6 +933,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js.ma packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.d.ts packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js.map +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.d.ts +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js.map packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts packages/app-mobile/components/NoteEditor/CodeMirror/theme.js packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map diff --git a/.gitignore b/.gitignore index 0a903fca3..1d1b9de1a 100644 --- a/.gitignore +++ b/.gitignore @@ -921,6 +921,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js.ma packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.d.ts packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js.map +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.d.ts +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js +packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js.map packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts packages/app-mobile/components/NoteEditor/CodeMirror/theme.js packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.ts index 33b77f2a2..f3270376d 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.ts @@ -7,6 +7,7 @@ import { initCodeMirror } from './CodeMirror'; import { themeStyle } from '@joplin/lib/theme'; import Setting from '@joplin/lib/models/Setting'; import { forceParsing } from '@codemirror/language'; +import loadLangauges from './testUtil/loadLanguages'; const createEditorSettings = (themeId: number) => { @@ -27,6 +28,7 @@ describe('CodeMirror', () => { const initialText = `${headerLineText}\nThis is a test.`; const editorSettings = createEditorSettings(Setting.THEME_LIGHT); + await loadLangauges(); const editor = initCodeMirror(document.body, initialText, editorSettings); // Force the generation of the syntax tree now. diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.ts index 8668c878b..d0182f134 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.ts @@ -10,10 +10,11 @@ describe('markdownCommands.bulletedVsChecklist', () => { const bulletedListPart = '- Test\n- This is a test.\n- 3\n- 4\n- 5'; const checklistPart = '- [ ] This is a checklist\n- [ ] with multiple items.\n- [ ] ☑'; const initialDocText = `${bulletedListPart}\n\n${checklistPart}`; + const expectedTags = ['BulletList', 'Task']; - it('should remove a checklist following a bulleted list without modifying the bulleted list', () => { - const editor = createEditor( - initialDocText, EditorSelection.cursor(bulletedListPart.length + 5) + it('should remove a checklist following a bulleted list without modifying the bulleted list', async () => { + const editor = await createEditor( + initialDocText, EditorSelection.cursor(bulletedListPart.length + 5), expectedTags ); toggleList(ListType.CheckList)(editor); @@ -22,9 +23,9 @@ describe('markdownCommands.bulletedVsChecklist', () => { ); }); - it('should remove an unordered list following a checklist without modifying the checklist', () => { - const editor = createEditor( - initialDocText, EditorSelection.cursor(bulletedListPart.length - 5) + it('should remove an unordered list following a checklist without modifying the checklist', async () => { + const editor = await createEditor( + initialDocText, EditorSelection.cursor(bulletedListPart.length - 5), expectedTags ); toggleList(ListType.UnorderedList)(editor); @@ -33,9 +34,9 @@ describe('markdownCommands.bulletedVsChecklist', () => { ); }); - it('should replace a selection of unordered and task lists with a correctly-numbered list', () => { - const editor = createEditor( - initialDocText, EditorSelection.range(0, initialDocText.length) + it('should replace a selection of unordered and task lists with a correctly-numbered list', async () => { + const editor = await createEditor( + initialDocText, EditorSelection.range(0, initialDocText.length), expectedTags ); toggleList(ListType.OrderedList)(editor); diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.ts index b69754176..29344fd3a 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.ts @@ -2,36 +2,18 @@ * @jest-environment jsdom */ -import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'; -import { EditorView } from '@codemirror/view'; +import { EditorSelection } from '@codemirror/state'; import { toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink, } from './markdownCommands'; -import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown'; -import { markdown } from '@codemirror/lang-markdown'; -import { MarkdownMathExtension } from './markdownMathParser'; -import { indentUnit } from '@codemirror/language'; - -// Creates and returns a minimal editor with markdown extensions -const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => { - return new EditorView({ - doc: initialText, - selection: EditorSelection.create([initialSelection]), - extensions: [ - markdown({ - extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt], - }), - indentUnit.of('\t'), - EditorState.tabSize.of(4), - ], - }); -}; +import createEditor from './testUtil/createEditor'; +import { blockMathTagName } from './markdownMathParser'; describe('markdownCommands', () => { - it('should bold/italicize everything selected', () => { + it('should bold/italicize everything selected', async () => { const initialDocText = 'Testing...'; - const editor = createEditor( - initialDocText, EditorSelection.range(0, initialDocText.length) + const editor = await createEditor( + initialDocText, EditorSelection.range(0, initialDocText.length), [] ); toggleBolded(editor); @@ -55,10 +37,10 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...'); }); - it('for a cursor, bolding, then italicizing, should produce a bold-italic region', () => { + it('for a cursor, bolding, then italicizing, should produce a bold-italic region', async () => { const initialDocText = ''; - const editor = createEditor( - initialDocText, EditorSelection.cursor(0) + const editor = await createEditor( + initialDocText, EditorSelection.cursor(0), [] ); toggleBolded(editor); @@ -73,9 +55,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('***Test*** Test'); }); - it('toggling math should both create and navigate out of math regions', () => { + it('toggling math should both create and navigate out of math regions', async () => { const initialDocText = 'Testing... '; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); expect(editor.state.doc.toString()).toBe('Testing... $$'); @@ -89,9 +71,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing... $3 + 3 \\neq 5$...'); }); - it('toggling inline code should both create and navigate out of an inline code region', () => { + it('toggling inline code should both create and navigate out of an inline code region', async () => { const initialDocText = 'Testing...\n\n'; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleCode(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); @@ -101,9 +83,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.'); }); - it('should set headers to the proper levels (when toggling)', () => { + it('should set headers to the proper levels (when toggling)', async () => { const initialDocText = 'Testing...\nThis is a test.'; - const editor = createEditor(initialDocText, EditorSelection.cursor(3)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(3), []); toggleHeaderLevel(1)(editor); @@ -127,11 +109,12 @@ describe('markdownCommands', () => { expect(mainSel.from).toBe('Testing...'.length); }); - it('headers should toggle properly within block quotes', () => { + it('headers should toggle properly within block quotes', async () => { const initialDocText = 'Testing...\n\n> This is a test.\n> ...a test'; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor('Testing...\n\n> This'.length) + EditorSelection.cursor('Testing...\n\n> This'.length), + ['Blockquote'] ); toggleHeaderLevel(1)(editor); @@ -150,69 +133,48 @@ describe('markdownCommands', () => { ); }); - // We need to disable this test because it randomly fails on CI. - // - // ● markdownCommands › block math should properly toggle within block quotes - // - // expect(received).toEqual(expected) // deep equality - // - // - Expected - 1 - // + Received + 3 - // - // Testing... - // - // - > This is a test. - // + > $$ - // + > This is$$ a test. - // > y = mx + b - // + > $$ - // > ...a test - // - // 179 | toggleMath(editor); - // 180 | mainSel = editor.state.selection.main; - // > 181 | expect(editor.state.doc.toString()).toEqual(initialDocText); - // | ^ - // 182 | expect(mainSel.from).toBe('Testing...\n\n'.length); - // 183 | expect(mainSel.to).toBe('Testing...\n\n> This is a test.\n> y = mx + b'.length); - // 184 | }); + it('block math should be created correctly within block quotes', async () => { + const initialDocText = 'Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test'; + const editor = await createEditor( + initialDocText, + EditorSelection.range( + 'Testing...\n\n> This'.length, + 'Testing...\n\n> This is a test.\n> y = mx + b'.length + ), + ['Blockquote'] + ); + toggleMath(editor); - // it('block math should properly toggle within block quotes', () => { - // const initialDocText = 'Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test'; - // const editor = createEditor( - // initialDocText, - // EditorSelection.range( - // 'Testing...\n\n> This'.length, - // 'Testing...\n\n> This is a test.\n> y = mx + b'.length - // ) - // ); + // Toggling math should surround the content in '$$'s + const mainSel = editor.state.selection.main; + expect(editor.state.doc.toString()).toEqual( + 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test' + ); + expect(mainSel.from).toBe('Testing...\n\n'.length); + expect(mainSel.to).toBe('Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$'.length); + }); - // toggleMath(editor); + it('block math should be correctly removed within block quotes', async () => { + const initialDocText = 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test'; - // // Toggling math should surround the content in '$$'s - // let mainSel = editor.state.selection.main; - // expect(editor.state.doc.toString()).toEqual( - // 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test' - // ); - // expect(mainSel.from).toBe('Testing...\n\n'.length); - // expect(mainSel.to).toBe('Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$'.length); + const editor = await createEditor( + initialDocText, + EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length), + ['Blockquote', blockMathTagName] + ); - // // Change to a cursor --- test cursor expansion - // editor.dispatch({ - // selection: EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length), - // }); + // Toggling math should remove the '$$'s + toggleMath(editor); + const mainSel = editor.state.selection.main; + expect(editor.state.doc.toString()).toEqual('Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test'); + expect(mainSel.from).toBe('Testing...\n\n'.length); + expect(mainSel.to).toBe('Testing...\n\n> This is a test.\n> y = mx + b'.length); + }); - // // Toggling math again should remove the '$$'s - // toggleMath(editor); - // mainSel = editor.state.selection.main; - // expect(editor.state.doc.toString()).toEqual(initialDocText); - // expect(mainSel.from).toBe('Testing...\n\n'.length); - // expect(mainSel.to).toBe('Testing...\n\n> This is a test.\n> y = mx + b'.length); - // }); - - it('updateLink should replace link titles and isolate URLs if no title is given', () => { + it('updateLink should replace link titles and isolate URLs if no title is given', async () => { const initialDocText = '[foo](http://example.com/)'; - const editor = createEditor(initialDocText, EditorSelection.cursor('[f'.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor('[f'.length), ['Link']); updateLink('bar', 'https://example.com/')(editor); expect(editor.state.doc.toString()).toBe( @@ -225,9 +187,9 @@ describe('markdownCommands', () => { ); }); - it('toggling math twice, starting on a line with content, should a math block', () => { + it('toggling math twice, starting on a line with content, should a math block', async () => { const initialDocText = 'Testing... '; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); toggleMath(editor); @@ -235,9 +197,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing... \n$$\nf(x) = ...\n$$'); }); - it('toggling math twice on an empty line should create an empty math block', () => { + it('toggling math twice on an empty line should create an empty math block', async () => { const initialDocText = 'Testing...\n\n'; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); toggleMath(editor); @@ -245,9 +207,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...\n\n$$\nf(x) = ...\n$$'); }); - it('toggling code twice on an empty line should create an empty code block', () => { + it('toggling code twice on an empty line should create an empty code block', async () => { const initialDocText = 'Testing...\n\n'; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); // Toggling code twice should create a block code region toggleCode(editor); @@ -259,9 +221,9 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...\n\nf(x) = ...\n'); }); - it('toggling math twice inside a block quote should produce an empty math block', () => { + it('toggling math twice inside a block quote should produce an empty math block', async () => { const initialDocText = '> Testing...> \n> '; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), ['Blockquote']); toggleMath(editor); toggleMath(editor); @@ -278,9 +240,9 @@ describe('markdownCommands', () => { expect(sel.to).toBe(editor.state.doc.length); }); - it('toggling inline code should both create and navigate out of an inline code region', () => { + it('toggling inline code should both create and navigate out of an inline code region', async () => { const initialDocText = 'Testing...\n\n'; - const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length)); + const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleCode(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.ts index 9fb071af4..02ca53089 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.ts @@ -10,25 +10,27 @@ import { ListType } from '../types'; import createEditor from './testUtil/createEditor'; describe('markdownCommands.toggleList', () => { - it('should remove the same type of list', () => { - const initialDocText = '- testing\n- this is a test'; + it('should remove the same type of list', async () => { + const initialDocText = '- testing\n- this is a `test`\n'; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor(5) + EditorSelection.cursor(5), + ['BulletList', 'InlineCode'] ); toggleList(ListType.UnorderedList)(editor); expect(editor.state.doc.toString()).toBe( - 'testing\nthis is a test' + 'testing\nthis is a `test`\n' ); }); - it('should insert a numbered list with correct numbering', () => { + it('should insert a numbered list with correct numbering', async () => { const initialDocText = 'Testing...\nThis is a test\nof list toggling...'; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor('Testing...\nThis is a'.length) + EditorSelection.cursor('Testing...\nThis is a'.length), + [] ); toggleList(ListType.OrderedList)(editor); @@ -47,12 +49,13 @@ describe('markdownCommands.toggleList', () => { ); }); - const numberedListText = '- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7'; + const unorderedListText = '- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7'; - it('should correctly replace an unordered list with a numbered list', () => { - const editor = createEditor( - numberedListText, - EditorSelection.cursor(numberedListText.length) + it('should correctly replace an unordered list with a numbered list', async () => { + const editor = await createEditor( + unorderedListText, + EditorSelection.cursor(unorderedListText.length), + ['BulletList'] ); toggleList(ListType.OrderedList)(editor); @@ -62,10 +65,11 @@ describe('markdownCommands.toggleList', () => { }); - it('should correctly replace an unordered list with a checklist', () => { - const editor = createEditor( - numberedListText, - EditorSelection.cursor(numberedListText.length) + it('should correctly replace an unordered list with a checklist', async () => { + const editor = await createEditor( + unorderedListText, + EditorSelection.cursor(unorderedListText.length), + ['BulletList'] ); toggleList(ListType.CheckList)(editor); @@ -74,13 +78,14 @@ describe('markdownCommands.toggleList', () => { ); }); - it('should properly toggle a sublist of a bulleted list', () => { + it('should properly toggle a sublist of a bulleted list', async () => { const preSubListText = '# List test\n * This\n * is\n'; const initialDocText = `${preSubListText}\t* a\n\t* test\n * of list toggling`; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor(preSubListText.length + '\t* a'.length) + EditorSelection.cursor(preSubListText.length + '\t* a'.length), + ['BulletList', 'ATXHeading1'] ); // Indentation should be preserved when changing list types @@ -94,6 +99,17 @@ describe('markdownCommands.toggleList', () => { expect(editor.state.selection.main.to).toBe( `${preSubListText}\t1. a\n\t2. test`.length ); + }); + + it('should not preserve indentation when removing sublists', async () => { + const preSubListText = '# List test\n * This\n * is\n'; + const initialDocText = `${preSubListText}\t1. a\n\t2. test\n * of list toggling`; + + const editor = await createEditor( + initialDocText, + EditorSelection.range(preSubListText.length, `${preSubListText}\t1. a\n\t2. test`.length), + ['ATXHeading1', 'BulletList', 'OrderedList'] + ); // Indentation should not be preserved when removing lists toggleList(ListType.OrderedList)(editor); @@ -102,51 +118,47 @@ describe('markdownCommands.toggleList', () => { '# List test\n * This\n * is\na\ntest\n * of list toggling' ); - // The below test: - // `expect(editor.state.doc.toString()).toBe(expectedChecklistPart)` - // randomly fails on CI, so disabling it for now. + // Put the cursor in the middle of the list + editor.dispatch({ selection: EditorSelection.cursor(preSubListText.length) }); + // Sublists should be changed + toggleList(ListType.CheckList)(editor); + const expectedChecklistPart = + '# List test\n - [ ] This\n - [ ] is\n - [ ] a\n - [ ] test\n - [ ] of list toggling'; + expect(editor.state.doc.toString()).toBe( + expectedChecklistPart + ); - // // Put the cursor in the middle of the list - // editor.dispatch({ selection: EditorSelection.cursor(preSubListText.length) }); + editor.dispatch({ selection: EditorSelection.cursor(editor.state.doc.length) }); + editor.dispatch(editor.state.replaceSelection('\n\n\n')); - // // Sublists should be changed - // toggleList(ListType.CheckList)(editor); - // const expectedChecklistPart = - // '# List test\n - [ ] This\n - [ ] is\n - [ ] a\n - [ ] test\n - [ ] of list toggling'; - // expect(editor.state.doc.toString()).toBe( - // expectedChecklistPart - // ); + // toggleList should also create a new list if the cursor is on an empty line. + toggleList(ListType.OrderedList)(editor); + editor.dispatch(editor.state.replaceSelection('Test.\n2. Test2\n3. Test3')); - // editor.dispatch({ selection: EditorSelection.cursor(editor.state.doc.length) }); - // editor.dispatch(editor.state.replaceSelection('\n\n\n')); + expect(editor.state.doc.toString()).toBe( + `${expectedChecklistPart}\n\n\n1. Test.\n2. Test2\n3. Test3` + ); - // // toggleList should also create a new list if the cursor is on an empty line. - // toggleList(ListType.OrderedList)(editor); - // editor.dispatch(editor.state.replaceSelection('Test.\n2. Test2\n3. Test3')); + toggleList(ListType.CheckList)(editor); + expect(editor.state.doc.toString()).toBe( + `${expectedChecklistPart}\n\n\n- [ ] Test.\n- [ ] Test2\n- [ ] Test3` + ); - // expect(editor.state.doc.toString()).toBe( - // `${expectedChecklistPart}\n\n\n1. Test.\n2. Test2\n3. Test3` - // ); - - // toggleList(ListType.CheckList)(editor); - // expect(editor.state.doc.toString()).toBe( - // `${expectedChecklistPart}\n\n\n- [ ] Test.\n- [ ] Test2\n- [ ] Test3` - // ); - - // // The entire checklist should have been selected (and thus will now be indented) - // increaseIndent(editor); - // expect(editor.state.doc.toString()).toBe( - // `${expectedChecklistPart}\n\n\n\t- [ ] Test.\n\t- [ ] Test2\n\t- [ ] Test3` - // ); + // The entire checklist should have been selected (and thus will now be indented) + increaseIndent(editor); + expect(editor.state.doc.toString()).toBe( + `${expectedChecklistPart}\n\n\n\t- [ ] Test.\n\t- [ ] Test2\n\t- [ ] Test3` + ); }); - it('should toggle a numbered list without changing its sublists', () => { + it('should toggle a numbered list without changing its sublists', async () => { const initialDocText = '1. Foo\n2. Bar\n3. Baz\n\t- Test\n\t- of\n\t- sublists\n4. Foo'; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor(0) + EditorSelection.cursor(0), + ['OrderedList', 'BulletList'] ); toggleList(ListType.CheckList)(editor); @@ -155,12 +167,13 @@ describe('markdownCommands.toggleList', () => { ); }); - it('should toggle a sublist without changing the parent list', () => { + it('should toggle a sublist without changing the parent list', async () => { const initialDocText = '1. This\n2. is\n3. '; - const editor = createEditor( + const editor = await createEditor( initialDocText, - EditorSelection.cursor(initialDocText.length) + EditorSelection.cursor(initialDocText.length), + ['OrderedList'] ); increaseIndent(editor); @@ -177,11 +190,12 @@ describe('markdownCommands.toggleList', () => { ); }); - it('should toggle lists properly within block quotes', () => { + it('should toggle lists properly within block quotes', async () => { const preSubListText = '> # List test\n> * This\n> * is\n'; const initialDocText = `${preSubListText}> \t* a\n> \t* test\n> * of list toggling`; - const editor = createEditor( - initialDocText, EditorSelection.cursor(preSubListText.length + 3) + const editor = await createEditor( + initialDocText, EditorSelection.cursor(preSubListText.length + 3), + ['BlockQuote', 'BulletList'] ); toggleList(ListType.OrderedList)(editor); diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.ts index 48228b086..16c518aa6 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.ts @@ -1,24 +1,17 @@ -import { markdown } from '@codemirror/lang-markdown'; +/** + * @jest-environment jsdom + */ + import { syntaxTree } from '@codemirror/language'; import { SyntaxNode } from '@lezer/common'; -import { EditorState } from '@codemirror/state'; -import { blockMathTagName, inlineMathTagName, MarkdownMathExtension } from './markdownMathParser'; -import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown'; -import forceFullParse from './testUtil/forceFullParse'; +import { EditorSelection, EditorState } from '@codemirror/state'; +import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './markdownMathParser'; + +import createEditor from './testUtil/createEditor'; // Creates an EditorState with math and markdown extensions -const createEditorState = (initialText: string): EditorState => { - const editorState = EditorState.create({ - doc: initialText, - extensions: [ - markdown({ - extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt], - }), - ], - }); - forceFullParse(editorState); - - return editorState; +const createEditorState = async (initialText: string, expectedTags: string[]): Promise => { + return (await createEditor(initialText, EditorSelection.cursor(0), expectedTags)).state; }; // Returns a list of all nodes with the given name in the given editor's syntax tree. @@ -38,31 +31,29 @@ const findNodesWithName = (editor: EditorState, nodeName: string) => { describe('markdownMathParser', () => { - // Disable flaky test - randomly fails on line `expect(inlineMathContentNodes.length).toBe(0);` + it('should parse inline math that contains space characters, numbers, and symbols', async () => { + const documentText = '$3 + 3$'; + const editor = await createEditorState(documentText, [inlineMathTagName, 'number']); + const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); + const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName); - // it('should parse inline math that contains space characters, numbers, and symbols', () => { - // const documentText = '$3 + 3$'; - // const editor = createEditorState(documentText); - // const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); - // const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName); + // There should only be one inline node + expect(inlineMathNodes.length).toBe(1); - // // There should only be one inline node - // expect(inlineMathNodes.length).toBe(1); + expect(inlineMathNodes[0].from).toBe(0); + expect(inlineMathNodes[0].to).toBe(documentText.length); - // expect(inlineMathNodes[0].from).toBe(0); - // expect(inlineMathNodes[0].to).toBe(documentText.length); + // The content tag should be replaced by the internal sTeX parser + expect(inlineMathContentNodes.length).toBe(0); + }); - // // The content tag should be replaced by the internal sTeX parser - // expect(inlineMathContentNodes.length).toBe(0); - // }); - - it('should parse comment within multi-word inline math', () => { + it('should parse comment within multi-word inline math', async () => { const beforeMath = '# Testing!\n\nThis is a test of '; const mathRegion = '$\\TeX % TeX Comment!$'; const afterMath = ' formatting.'; const documentText = `${beforeMath}${mathRegion}${afterMath}`; - const editor = createEditorState(documentText); + const editor = await createEditorState(documentText, [inlineMathTagName, 'comment']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); const commentNodes = findNodesWithName(editor, 'comment'); @@ -75,30 +66,30 @@ describe('markdownMathParser', () => { expect(inlineMathNodes[0].to).toBe(beforeMath.length + mathRegion.length); }); - it('shouldn\'t start inline math if there is no ending $', () => { - const documentText = 'This is a $test\n\nof inline math$...'; - const editor = createEditorState(documentText); + it('shouldn\'t start inline math if there is no ending $', async () => { + const documentText = '*This* is a $test\n\nof inline math$...'; + const editor = await createEditorState(documentText, ['Emphasis']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); // Math should end if there is no matching '$'. expect(inlineMathNodes.length).toBe(0); }); - it('shouldn\'t start if math would have a space just after the $', () => { - const documentText = 'This is a $ test of inline math$...\n\n$Testing... $...'; - const editor = createEditorState(documentText); + it('shouldn\'t start if math would have a space just after the $', async () => { + const documentText = 'This *is* a $ test of inline math$...\n\n$Testing... $...'; + const editor = await createEditorState(documentText, ['Emphasis']); expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0); }); - it('shouldn\'t start inline math if $ is escaped', () => { - const documentText = 'This is a \\$test of inline math$...'; - const editor = createEditorState(documentText); + it('shouldn\'t start inline math if $ is escaped', async () => { + const documentText = 'This is a \\$test of inline math$... **Testing...**'; + const editor = await createEditorState(documentText, ['StrongEmphasis']); expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0); }); - it('should correctly parse document containing just block math', () => { - const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\}\n$$'; - const editor = createEditorState(documentText); + it('should correctly parse document containing just block math', async () => { + const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\} % Comment\n$$'; + const editor = await createEditorState(documentText, [blockMathTagName, 'comment']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); @@ -109,10 +100,10 @@ describe('markdownMathParser', () => { expect(blockMathNodes[0].to).toBe(documentText.length); }); - it('should correctly parse comment in block math', () => { + it('should correctly parse comment in block math', async () => { const startingText = '$$ % Testing...\n\t\\text{Test.}\n$$'; const afterMath = '\nTest.'; - const editor = createEditorState(startingText + afterMath); + const editor = await createEditorState(startingText + afterMath, ['comment', blockMathTagName]); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); const texParserComments = findNodesWithName(editor, 'comment'); @@ -130,10 +121,10 @@ describe('markdownMathParser', () => { }); }); - it('should extend block math without ending tag to end of document', () => { + it('should extend block math without ending tag to end of document', async () => { const beforeMath = '# Testing...\n\n'; - const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6`; - const editor = createEditorState(documentText); + const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6 % Comment`; + const editor = await createEditorState(documentText, ['ATXHeading1', blockMathTagName, 'comment']); const blockMathNodes = findNodesWithName(editor, blockMathTagName); expect(blockMathNodes.length).toBe(1); @@ -141,9 +132,9 @@ describe('markdownMathParser', () => { expect(blockMathNodes[0].to).toBe(documentText.length); }); - it('should parse block math declared on a single line', () => { + it('should parse block math declared on a single line', async () => { const documentText = '$$ Test. $$'; - const editor = createEditorState(documentText); + const editor = await createEditorState(documentText, [blockMathTagName]); const blockMathNodes = findNodesWithName(editor, blockMathTagName); expect(blockMathNodes.length).toBe(1); diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.ts index c4a8c2b4d..2c01accef 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.ts @@ -1,13 +1,19 @@ import { markdown } from '@codemirror/lang-markdown'; import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown'; -import { indentUnit } from '@codemirror/language'; +import { indentUnit, syntaxTree } from '@codemirror/language'; import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { MarkdownMathExtension } from '../markdownMathParser'; import forceFullParse from './forceFullParse'; +import loadLangauges from './loadLanguages'; + +// Creates and returns a minimal editor with markdown extensions. Waits to return the editor +// until all syntax tree tags in `expectedSyntaxTreeTags` exist. +const createEditor = async ( + initialText: string, initialSelection: SelectionRange, expectedSyntaxTreeTags: string[] +): Promise => { + await loadLangauges(); -// Creates and returns a minimal editor with markdown extensions -const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => { const editor = new EditorView({ doc: initialText, selection: EditorSelection.create([initialSelection]), @@ -20,7 +26,39 @@ const createEditor = (initialText: string, initialSelection: SelectionRange): Ed ], }); - forceFullParse(editor.state); + let sawExpectedTagCount = 0; + while (sawExpectedTagCount < expectedSyntaxTreeTags.length) { + forceFullParse(editor.state); + + sawExpectedTagCount = 0; + const seenTags = new Set(); + + syntaxTree(editor.state).iterate({ + from: 0, + to: editor.state.doc.length, + enter: (node) => { + for (const expectedTag of expectedSyntaxTreeTags) { + if (node.name === expectedTag) { + seenTags.add(node.name); + sawExpectedTagCount ++; + break; + } + } + }, + }); + + if (sawExpectedTagCount < expectedSyntaxTreeTags.length) { + const missingTags = expectedSyntaxTreeTags.filter(tagName => { + return !seenTags.has(tagName); + }); + console.warn(`Didn't find all expected tags. Missing ${missingTags}. Retrying...`); + + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + } + } + return editor; }; diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.ts new file mode 100644 index 000000000..910bf3bf1 --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.ts @@ -0,0 +1,12 @@ +import syntaxHighlightingLanguages from '../syntaxHighlightingLanguages'; + +// Ensure languages we use are loaded. Without this, tests may randomly fail (LanguageDescriptions +// are loaded asyncronously, in the background). +const loadLangauges = async () => { + const allLanguages = syntaxHighlightingLanguages; + + for (const lang of allLanguages) { + await lang.load(); + } +}; +export default loadLangauges; diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index d39c7c579..a03f25233 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -81,11 +81,11 @@ "@codemirror/lang-markdown": "6.0.5", "@codemirror/lang-php": "6.0.1", "@codemirror/lang-rust": "6.0.1", - "@codemirror/language": "6.3.1", + "@codemirror/language": "6.3.2", "@codemirror/legacy-modes": "6.3.1", "@codemirror/search": "6.2.3", "@codemirror/state": "6.1.4", - "@codemirror/view": "6.6.0", + "@codemirror/view": "6.7.1", "@joplin/tools": "~2.10", "@lezer/highlight": "1.1.3", "@types/fs-extra": "9.0.13", diff --git a/yarn.lock b/yarn.lock index 3aa28aa45..c701b8201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3173,8 +3173,8 @@ __metadata: linkType: hard "@codemirror/autocomplete@npm:^6.0.0": - version: 6.3.4 - resolution: "@codemirror/autocomplete@npm:6.3.4" + version: 6.4.0 + resolution: "@codemirror/autocomplete@npm:6.4.0" dependencies: "@codemirror/language": ^6.0.0 "@codemirror/state": ^6.0.0 @@ -3185,7 +3185,7 @@ __metadata: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.0.0 - checksum: dafb6b3dee11551ed7a2ec1d20fa05641abefe2e0b5da045d4a3383146bb04f0b9650448a378a5921cc183944d626482a608b71f3da5a036a881a873006b8dbf + checksum: 3470fee01da60d3d71b8b4f8728629c0f0441e704b8b828592f98c000d75fdb2c9077727e82685626cf45b95cadbc0c1a03968261df2f0cfb4162418b5f4dd1f languageName: node linkType: hard @@ -3250,7 +3250,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/lang-javascript@npm:6.1.1, @codemirror/lang-javascript@npm:^6.0.0": +"@codemirror/lang-javascript@npm:6.1.1": version: 6.1.1 resolution: "@codemirror/lang-javascript@npm:6.1.1" dependencies: @@ -3265,6 +3265,21 @@ __metadata: languageName: node linkType: hard +"@codemirror/lang-javascript@npm:^6.0.0": + version: 6.1.2 + resolution: "@codemirror/lang-javascript@npm:6.1.2" + dependencies: + "@codemirror/autocomplete": ^6.0.0 + "@codemirror/language": ^6.0.0 + "@codemirror/lint": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + "@lezer/javascript": ^1.0.0 + checksum: f4336b7efd44e4158b9979f0c23918184c897d0fe3e40b5414bd9243a9899ecdba4dfe13970fe5024a1894579af80cb4c5dd574c6c2b7bd7ff06d8c8cb88616b + languageName: node + linkType: hard + "@codemirror/lang-markdown@npm:6.0.5": version: 6.0.5 resolution: "@codemirror/lang-markdown@npm:6.0.5" @@ -3302,9 +3317,9 @@ __metadata: languageName: node linkType: hard -"@codemirror/language@npm:6.3.1, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.3.0": - version: 6.3.1 - resolution: "@codemirror/language@npm:6.3.1" +"@codemirror/language@npm:6.3.2, @codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.3.0": + version: 6.3.2 + resolution: "@codemirror/language@npm:6.3.2" dependencies: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 @@ -3312,7 +3327,7 @@ __metadata: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: 349b9806e1e2ce5d99ba1f5815cc4772e6032f68c95718594e8335196ef0686bc6378db7cdd5f0fda57ba068eebf0ee413bb336e32cc1ff958a743190a0266da + checksum: b70ed9b85d0bea79181c86e88a1f5c0bada30680ee1fe6a68efc01bc037c3d14f94a83602fc46cc4b4393589605ef7e986ed5174443502f3365dd61f883894fa languageName: node linkType: hard @@ -3354,14 +3369,14 @@ __metadata: languageName: node linkType: hard -"@codemirror/view@npm:6.6.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.2, @codemirror/view@npm:^6.6.0": - version: 6.6.0 - resolution: "@codemirror/view@npm:6.6.0" +"@codemirror/view@npm:6.7.1, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.2, @codemirror/view@npm:^6.6.0": + version: 6.7.1 + resolution: "@codemirror/view@npm:6.7.1" dependencies: "@codemirror/state": ^6.1.4 style-mod: ^4.0.0 w3c-keyname: ^2.2.4 - checksum: 9b007eedcf13e94ec7d9c30ee302e1a1fcd382bef2481bd9afa3a116458652983e745b40494eb29d80df1dca8f99e91dcb1e4eba37670c2553ffc90bef0933e7 + checksum: 75a5846d61e63027e9bf1dfd0b507932934cb7650b7959c1191e68b161eb1756e9773f964c4331970b51864aef8f7954bc5cc8fdb51b0f6533de6c20568833ed languageName: node linkType: hard @@ -4662,11 +4677,11 @@ __metadata: "@codemirror/lang-markdown": 6.0.5 "@codemirror/lang-php": 6.0.1 "@codemirror/lang-rust": 6.0.1 - "@codemirror/language": 6.3.1 + "@codemirror/language": 6.3.2 "@codemirror/legacy-modes": 6.3.1 "@codemirror/search": 6.2.3 "@codemirror/state": 6.1.4 - "@codemirror/view": 6.6.0 + "@codemirror/view": 6.7.1 "@joplin/lib": ~2.10 "@joplin/react-native-saf-x": ~2.10 "@joplin/renderer": ~2.10 @@ -6023,12 +6038,12 @@ __metadata: linkType: hard "@lezer/css@npm:^1.0.0, @lezer/css@npm:^1.1.0": - version: 1.1.0 - resolution: "@lezer/css@npm:1.1.0" + version: 1.1.1 + resolution: "@lezer/css@npm:1.1.1" dependencies: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 - checksum: 5d2a176d7f4cf5076d8841af9b7bcafcbad5dd1b8f46fa1ad56c0fbf76f4bd4cd4ee0b1c4f1f1c9f8dba4fffb88908e64b5d7919c8706b35f575ddff8512ef31 + checksum: a7e4893aacaa7f26d5679c77a640f401b37d14155cb54863aa91b59dfd220b280360a341c0fedafc65d31101de13a5ae33cf3876c352f2da528344dafdc9b3d7 languageName: node linkType: hard @@ -6042,13 +6057,13 @@ __metadata: linkType: hard "@lezer/html@npm:^1.1.0": - version: 1.2.0 - resolution: "@lezer/html@npm:1.2.0" + version: 1.3.0 + resolution: "@lezer/html@npm:1.3.0" dependencies: "@lezer/common": ^1.0.0 "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 - checksum: 737f6884328845100575c3bb9b0add622d00233d9d75f6bd201d37e31b990af371984b7ab91681bfe258234b77d486bc97f61a8ebdb4bd70a942f06a22b1aac1 + checksum: e6efde94614a5b7ebf2713b244a110ef9025369561c7bf42fe6dd8f5877d2ee0c71f894b8b43d1284d23bf429fd3688ec3b6b0c2b8702df366c2b5e5cedc4c19 languageName: node linkType: hard @@ -6063,12 +6078,12 @@ __metadata: linkType: hard "@lezer/javascript@npm:^1.0.0": - version: 1.3.1 - resolution: "@lezer/javascript@npm:1.3.1" + version: 1.4.0 + resolution: "@lezer/javascript@npm:1.4.0" dependencies: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 - checksum: bcf1a2ac84198f7caedf320d5222b6f4d39ece62d939ebe02b461bb175027c9b66d7be7feba51d3d8317bdd972c4fbdde3d9edbd60fa342b9e97db8ca1b63922 + checksum: 36c64e8530feef9b937cf75f8833aa8c0f5c8c0812c55c53a133d1af5deb491dd80084397d5773e873db90ff717aede25b45fa827eead66400cb81b097567c42 languageName: node linkType: hard