summaryrefslogtreecommitdiff
path: root/spec/frontend/editor/source_editor_markdown_ext_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/editor/source_editor_markdown_ext_spec.js')
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js385
1 files changed, 381 insertions, 4 deletions
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 943e21250b4..48ccc10e486 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,16 +1,36 @@
-import { Range, Position } from 'monaco-editor';
+import MockAdapter from 'axios-mock-adapter';
+import { Range, Position, editor as monacoEditor } from 'monaco-editor';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+} from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import SourceEditor from '~/editor/source_editor';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import syntaxHighlight from '~/syntax_highlight';
+
+jest.mock('~/syntax_highlight');
+jest.mock('~/flash');
describe('Markdown Extension for Source Editor', () => {
let editor;
let instance;
let editorEl;
+ let panelSpy;
+ let mockAxios;
+ const projectPath = 'fooGroup/barProj';
const firstLine = 'This is a';
const secondLine = 'multiline';
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
- const filePath = 'foo.md';
+ const plaintextPath = 'foo.txt';
+ const markdownPath = 'foo.md';
+ const responseData = '<div>FooBar</div>';
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
@@ -22,21 +42,378 @@ describe('Markdown Extension for Source Editor', () => {
const selectionToString = () => instance.getSelection().toString();
const positionToString = () => instance.getPosition().toString();
+ const togglePreview = async () => {
+ instance.togglePreview();
+ await waitForPromises();
+ };
+
beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new SourceEditor();
instance = editor.createInstance({
el: editorEl,
- blobPath: filePath,
+ blobPath: markdownPath,
blobContent: text,
});
- editor.use(new EditorMarkdownExtension());
+ editor.use(new EditorMarkdownExtension({ instance, projectPath }));
+ panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
});
afterEach(() => {
instance.dispose();
editorEl.remove();
+ mockAxios.restore();
+ });
+
+ it('sets up the instance', () => {
+ expect(instance.preview).toEqual({
+ el: undefined,
+ action: expect.any(Object),
+ shown: false,
+ modelChangeListener: undefined,
+ });
+ expect(instance.projectPath).toBe(projectPath);
+ });
+
+ describe('model language changes listener', () => {
+ let cleanupSpy;
+ let actionSpy;
+
+ beforeEach(async () => {
+ cleanupSpy = jest.spyOn(instance, 'cleanup');
+ actionSpy = jest.spyOn(instance, 'setupPreviewAction');
+ await togglePreview();
+ });
+
+ it('cleans up when switching away from markdown', () => {
+ expect(instance.cleanup).not.toHaveBeenCalled();
+ expect(instance.setupPreviewAction).not.toHaveBeenCalled();
+
+ instance.updateModelLanguage(plaintextPath);
+
+ expect(cleanupSpy).toHaveBeenCalled();
+ expect(actionSpy).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ oldLanguage | newLanguage | setupCalledTimes
+ ${'plaintext'} | ${'markdown'} | ${1}
+ ${'markdown'} | ${'markdown'} | ${0}
+ ${'markdown'} | ${'plaintext'} | ${0}
+ ${'markdown'} | ${undefined} | ${0}
+ ${undefined} | ${'markdown'} | ${1}
+ `(
+ 'correctly handles re-enabling of the action when switching from $oldLanguage to $newLanguage',
+ ({ oldLanguage, newLanguage, setupCalledTimes } = {}) => {
+ expect(actionSpy).not.toHaveBeenCalled();
+ instance.updateModelLanguage(oldLanguage);
+ instance.updateModelLanguage(newLanguage);
+ expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
+ },
+ );
+ });
+
+ describe('model change listener', () => {
+ let cleanupSpy;
+ let actionSpy;
+
+ beforeEach(() => {
+ cleanupSpy = jest.spyOn(instance, 'cleanup');
+ actionSpy = jest.spyOn(instance, 'setupPreviewAction');
+ instance.togglePreview();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('does not do anything if there is no model', () => {
+ instance.setModel(null);
+
+ expect(cleanupSpy).not.toHaveBeenCalled();
+ expect(actionSpy).not.toHaveBeenCalled();
+ });
+
+ it('cleans up the preview when the model changes', () => {
+ instance.setModel(monacoEditor.createModel('foo'));
+ expect(cleanupSpy).toHaveBeenCalled();
+ });
+
+ it.each`
+ language | setupCalledTimes
+ ${'markdown'} | ${1}
+ ${'plaintext'} | ${0}
+ ${undefined} | ${0}
+ `(
+ 'correctly handles actions when the new model is $language',
+ ({ language, setupCalledTimes } = {}) => {
+ instance.setModel(monacoEditor.createModel('foo', language));
+
+ expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
+ },
+ );
+ });
+
+ describe('cleanup', () => {
+ beforeEach(async () => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ await togglePreview();
+ });
+
+ it('disposes the modelChange listener and does not fetch preview on content changes', () => {
+ expect(instance.preview.modelChangeListener).toBeDefined();
+ jest.spyOn(instance, 'fetchPreview');
+
+ instance.cleanup();
+ instance.setValue('Foo Bar');
+ jest.advanceTimersByTime(EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY);
+
+ expect(instance.fetchPreview).not.toHaveBeenCalled();
+ });
+
+ it('removes the contextual menu action', () => {
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
+
+ instance.cleanup();
+
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null);
+ });
+
+ it('toggles the `shown` flag', () => {
+ expect(instance.preview.shown).toBe(true);
+ instance.cleanup();
+ expect(instance.preview.shown).toBe(false);
+ });
+
+ it('toggles the panel only if the preview is visible', () => {
+ const { el: previewEl } = instance.preview;
+ const parentEl = previewEl.parentElement;
+
+ expect(previewEl).toBeVisible();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true);
+
+ instance.cleanup();
+ expect(previewEl).toBeHidden();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+
+ instance.cleanup();
+ expect(previewEl).toBeHidden();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ });
+
+ it('toggles the layout only if the preview is visible', () => {
+ const { width } = instance.getLayoutInfo();
+
+ expect(instance.preview.shown).toBe(true);
+
+ instance.cleanup();
+
+ const { width: newWidth } = instance.getLayoutInfo();
+ expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
+
+ instance.cleanup();
+ expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
+ });
+ });
+
+ describe('fetchPreview', () => {
+ const group = 'foo';
+ const project = 'bar';
+ const setData = (path, g, p) => {
+ instance.projectPath = path;
+ document.body.setAttribute('data-group', g);
+ document.body.setAttribute('data-project', p);
+ };
+ const fetchPreview = async () => {
+ instance.fetchPreview();
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ });
+
+ it('correctly fetches preview based on projectPath', async () => {
+ setData(projectPath, group, project);
+ await fetchPreview();
+ expect(mockAxios.history.post[0].url).toBe(`/${projectPath}/preview_markdown`);
+ expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
+ });
+
+ it('correctly fetches preview based on group and project data attributes', async () => {
+ setData(undefined, group, project);
+ await fetchPreview();
+ expect(mockAxios.history.post[0].url).toBe(`/${group}/${project}/preview_markdown`);
+ expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
+ });
+
+ it('puts the fetched content into the preview DOM element', async () => {
+ instance.preview.el = editorEl.parentElement;
+ await fetchPreview();
+ expect(instance.preview.el.innerHTML).toEqual(responseData);
+ });
+
+ it('applies syntax highlighting to the preview content', async () => {
+ instance.preview.el = editorEl.parentElement;
+ await fetchPreview();
+ expect(syntaxHighlight).toHaveBeenCalled();
+ });
+
+ it('catches the errors when fetching the preview', async () => {
+ mockAxios.onPost().reply(500);
+
+ await fetchPreview();
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('setupPreviewAction', () => {
+ it('adds the contextual menu action', () => {
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
+ });
+
+ it('does not set up action if one already exists', () => {
+ jest.spyOn(instance, 'addAction').mockImplementation();
+
+ instance.setupPreviewAction();
+ expect(instance.addAction).not.toHaveBeenCalled();
+ });
+
+ it('toggles preview when the action is triggered', () => {
+ jest.spyOn(instance, 'togglePreview').mockImplementation();
+
+ expect(instance.togglePreview).not.toHaveBeenCalled();
+
+ const action = instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID);
+ action.run();
+
+ expect(instance.togglePreview).toHaveBeenCalled();
+ });
+ });
+
+ describe('togglePreview', () => {
+ beforeEach(() => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ });
+
+ it('toggles preview flag on instance', () => {
+ expect(instance.preview.shown).toBe(false);
+
+ instance.togglePreview();
+ expect(instance.preview.shown).toBe(true);
+
+ instance.togglePreview();
+ expect(instance.preview.shown).toBe(false);
+ });
+
+ describe('panel DOM element set up', () => {
+ it('sets up an element to contain the preview and stores it on instance', () => {
+ expect(instance.preview.el).toBeUndefined();
+
+ instance.togglePreview();
+
+ expect(instance.preview.el).toBeDefined();
+ expect(instance.preview.el.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS)).toBe(
+ true,
+ );
+ });
+
+ it('re-uses existing preview DOM element on repeated calls', () => {
+ instance.togglePreview();
+ const origPreviewEl = instance.preview.el;
+ instance.togglePreview();
+
+ expect(instance.preview.el).toBe(origPreviewEl);
+ });
+
+ it('hides the preview DOM element by default', () => {
+ panelSpy.mockImplementation();
+ instance.togglePreview();
+ expect(instance.preview.el.style.display).toBe('none');
+ });
+ });
+
+ describe('preview layout setup', () => {
+ it('sets correct preview layout', () => {
+ jest.spyOn(instance, 'layout');
+ const { width, height } = instance.getLayoutInfo();
+
+ instance.togglePreview();
+
+ expect(instance.layout).toHaveBeenCalledWith({
+ width: width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ height,
+ });
+ });
+ });
+
+ describe('preview panel', () => {
+ it('toggles preview CSS class on the editor', () => {
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ instance.togglePreview();
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ true,
+ );
+ instance.togglePreview();
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ });
+
+ it('toggles visibility of the preview DOM element', async () => {
+ await togglePreview();
+ expect(instance.preview.el.style.display).toBe('block');
+ await togglePreview();
+ expect(instance.preview.el.style.display).toBe('none');
+ });
+
+ describe('hidden preview DOM element', () => {
+ it('listens to model changes and re-fetches preview', async () => {
+ expect(mockAxios.history.post).toHaveLength(0);
+ await togglePreview();
+ expect(mockAxios.history.post).toHaveLength(1);
+
+ instance.setValue('New Value');
+ await waitForPromises();
+ expect(mockAxios.history.post).toHaveLength(2);
+ });
+
+ it('stores disposable listener for model changes', async () => {
+ expect(instance.preview.modelChangeListener).toBeUndefined();
+ await togglePreview();
+ expect(instance.preview.modelChangeListener).toBeDefined();
+ });
+ });
+
+ describe('already visible preview', () => {
+ beforeEach(async () => {
+ await togglePreview();
+ mockAxios.resetHistory();
+ });
+
+ it('does not re-fetch the preview', () => {
+ instance.togglePreview();
+ expect(mockAxios.history.post).toHaveLength(0);
+ });
+
+ it('disposes the model change event listener', () => {
+ const disposeSpy = jest.fn();
+ instance.preview.modelChangeListener = {
+ dispose: disposeSpy,
+ };
+ instance.togglePreview();
+ expect(disposeSpy).toHaveBeenCalled();
+ });
+ });
+ });
});
describe('getSelectedText', () => {