Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/editor/source_editor_instance_spec.js')
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js387
1 files changed, 387 insertions, 0 deletions
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
new file mode 100644
index 00000000000..87b20a4ba73
--- /dev/null
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -0,0 +1,387 @@
+import { editor as monacoEditor } from 'monaco-editor';
+import {
+ EDITOR_EXTENSION_NAMING_CONFLICT_ERROR,
+ EDITOR_EXTENSION_NO_DEFINITION_ERROR,
+ EDITOR_EXTENSION_DEFINITION_TYPE_ERROR,
+ EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
+ EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
+} from '~/editor/constants';
+import Instance from '~/editor/source_editor_instance';
+import { sprintf } from '~/locale';
+import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
+
+describe('Source Editor Instance', () => {
+ let seInstance;
+
+ const defSetupOptions = { foo: 'bar' };
+ const fullExtensionsArray = [
+ { definition: MyClassExtension },
+ { definition: MyFnExtension },
+ { definition: MyConstExt },
+ ];
+ const fullExtensionsArrayWithOptions = [
+ { definition: MyClassExtension, setupOptions: defSetupOptions },
+ { definition: MyFnExtension, setupOptions: defSetupOptions },
+ { definition: MyConstExt, setupOptions: defSetupOptions },
+ ];
+
+ const fooFn = jest.fn();
+ class DummyExt {
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ fooFn,
+ };
+ }
+ }
+
+ afterEach(() => {
+ seInstance = undefined;
+ });
+
+ it('sets up the registry for the methods coming from extensions', () => {
+ seInstance = new Instance();
+ expect(seInstance.methods).toBeDefined();
+
+ seInstance.use({ definition: MyClassExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ });
+
+ seInstance.use({ definition: MyFnExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ fnExtMethod: 'MyFnExtension',
+ });
+ });
+
+ describe('proxy', () => {
+ it('returns prop from an extension if extension provides it', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it('returns props from SE instance itself if no extension provides the prop', () => {
+ seInstance = new Instance({
+ use: fooFn,
+ });
+ jest.spyOn(seInstance, 'use').mockImplementation(() => {});
+ expect(seInstance.use).not.toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.use();
+ expect(seInstance.use).toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ });
+
+ it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
+ seInstance = new Instance({
+ fooFn,
+ });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+ });
+
+ describe('public API', () => {
+ it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
+ seInstance = new Instance();
+ expect(seInstance[method]).toBeDefined();
+ });
+
+ describe('use', () => {
+ it('extends the SE instance with methods provided by an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it.each`
+ extensions | expectedProps
+ ${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
+ ${{ definition: MyFnExtension }} | ${['fnExtMethod']}
+ ${{ definition: MyConstExt }} | ${['constExtMethod']}
+ ${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ `(
+ 'Should register $expectedProps when extension is "$extensions"',
+ ({ extensions, expectedProps }) => {
+ seInstance = new Instance();
+ expect(seInstance.extensionsAPI).toHaveLength(0);
+
+ seInstance.use(extensions);
+
+ expect(seInstance.extensionsAPI).toEqual(expectedProps);
+ },
+ );
+
+ it.each`
+ definition | preInstalledExtDefinition | expectedErrorProp
+ ${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
+ ${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
+ ${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
+ ${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
+ ${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
+ `(
+ 'logs the naming conflict error when registering $definition',
+ ({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
+ seInstance = new Instance();
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ if (preInstalledExtDefinition) {
+ seInstance.use({ definition: preInstalledExtDefinition });
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+
+ seInstance.use({ definition });
+
+ if (expectedErrorProp) {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.stringContaining(
+ sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop: expectedErrorProp }),
+ ),
+ );
+ } else {
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ it.each`
+ extensions | thrownError
+ ${''} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${undefined} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{}} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ foo: 'bar' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: '' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: undefined }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: [] }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: {} }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: { foo: 'bar' } }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ `(
+ 'Should throw $thrownError when extension is "$extensions"',
+ ({ extensions, thrownError }) => {
+ seInstance = new Instance();
+ const useExtension = () => {
+ seInstance.use(extensions);
+ };
+ expect(useExtension).toThrowError(thrownError);
+ },
+ );
+
+ describe('global extensions registry', () => {
+ let extensionStore;
+
+ beforeEach(() => {
+ extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ });
+
+ it('stores _instances_ of the used extensions in a global registry', () => {
+ const extension = seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.entries().next().value).toEqual(['MyClassExtension', extension]);
+ });
+
+ it('does not duplicate entries in the registry', () => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({ definition: MyClassExtension });
+ seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.set).toHaveBeenCalledTimes(1);
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ });
+
+ it.each`
+ desc | currentSetupOptions | newSetupOptions | expectedCallTimes
+ ${'updates'} | ${undefined} | ${defSetupOptions} | ${2}
+ ${'updates'} | ${defSetupOptions} | ${undefined} | ${2}
+ ${'updates'} | ${{ foo: 'bar' }} | ${{ foo: 'new' }} | ${2}
+ ${'does not update'} | ${undefined} | ${undefined} | ${1}
+ ${'does not update'} | ${{}} | ${{}} | ${1}
+ ${'does not update'} | ${defSetupOptions} | ${defSetupOptions} | ${1}
+ `(
+ '$desc the extensions entry when setupOptions "$currentSetupOptions" get changed to "$newSetupOptions"',
+ ({ currentSetupOptions, newSetupOptions, expectedCallTimes }) => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: currentSetupOptions,
+ });
+ const extension2 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: newSetupOptions,
+ });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
+ if (expectedCallTimes > 1) {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
+ } else {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ }
+ },
+ );
+ });
+ });
+
+ describe('unuse', () => {
+ it.each`
+ unuseExtension | thrownError
+ ${undefined} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${''} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${{}} | ${sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name: '' })}
+ ${[]} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ `(
+ `Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
+ ({ unuseExtension, thrownError }) => {
+ seInstance = new Instance();
+ const unuse = () => {
+ seInstance.unuse(unuseExtension);
+ };
+ expect(unuse).toThrowError(thrownError);
+ },
+ );
+
+ it.each`
+ initExtensions | unuseExtensionIndex | remainingAPI
+ ${{ definition: MyClassExtension }} | ${0} | ${[]}
+ ${{ definition: MyFnExtension }} | ${0} | ${[]}
+ ${{ definition: MyConstExt }} | ${0} | ${[]}
+ ${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
+ `(
+ 'un-registers properties introduced by single extension $unuseExtension',
+ ({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(initExtensions);
+
+ if (Array.isArray(initExtensions)) {
+ seInstance.unuse(extensions[unuseExtensionIndex]);
+ } else {
+ seInstance.unuse(extensions);
+ }
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it.each`
+ unuseExtensionIndex | remainingAPI
+ ${[0, 1]} | ${['constExtMethod']}
+ ${[0, 2]} | ${['fnExtMethod']}
+ ${[1, 2]} | ${['shared', 'classExtMethod']}
+ `(
+ 'un-registers properties introduced by multiple extensions $unuseExtension',
+ ({ unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(fullExtensionsArray);
+ const extensionsToUnuse = extensions.filter((ext, index) =>
+ unuseExtensionIndex.includes(index),
+ );
+
+ seInstance.unuse(extensionsToUnuse);
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it('it does not remove entry from the global registry to keep for potential future re-use', () => {
+ const extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ const extensions = seInstance.use(fullExtensionsArray);
+ const verifyExpectations = () => {
+ const entries = extensionStore.entries();
+ const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
+ expect(extensionStore.size).toBe(mockExtensions.length);
+ mockExtensions.forEach((ext, index) => {
+ expect(entries.next().value).toEqual([ext, extensions[index]]);
+ });
+ };
+
+ verifyExpectations();
+ seInstance.unuse(extensions);
+ verifyExpectations();
+ });
+ });
+
+ describe('updateModelLanguage', () => {
+ let instanceModel;
+
+ beforeEach(() => {
+ instanceModel = monacoEditor.createModel('');
+ seInstance = new Instance({
+ getModel: () => instanceModel,
+ });
+ });
+
+ it.each`
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.md'} | ${'markdown'}
+ ${'foo.rb'} | ${'ruby'}
+ ${''} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'test.nonexistingext'} | ${'plaintext'}
+ `(
+ 'changes language of an attached model to "$expectedLanguage" when filepath is "$path"',
+ ({ path, expectedLanguage }) => {
+ seInstance.updateModelLanguage(path);
+ expect(instanceModel.getLanguageIdentifier().language).toBe(expectedLanguage);
+ },
+ );
+ });
+
+ describe('extensions life-cycle callbacks', () => {
+ const onSetup = jest.fn().mockImplementation(() => {});
+ const onUse = jest.fn().mockImplementation(() => {});
+ const onBeforeUnuse = jest.fn().mockImplementation(() => {});
+ const onUnuse = jest.fn().mockImplementation(() => {});
+ const MyFullExtWithCallbacks = () => {
+ return {
+ onSetup,
+ onUse,
+ onBeforeUnuse,
+ onUnuse,
+ };
+ };
+
+ it('passes correct arguments to callback fns when using an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ expect(onSetup).toHaveBeenCalledWith(defSetupOptions, seInstance);
+ expect(onUse).toHaveBeenCalledWith(seInstance);
+ });
+
+ it('passes correct arguments to callback fns when un-using an extension', () => {
+ seInstance = new Instance();
+ const extension = seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ seInstance.unuse(extension);
+ expect(onBeforeUnuse).toHaveBeenCalledWith(seInstance);
+ expect(onUnuse).toHaveBeenCalledWith(seInstance);
+ });
+ });
+ });
+});