diff options
Diffstat (limited to 'spec/frontend')
6 files changed, 194 insertions, 58 deletions
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js index 05fa0f79ef0..02e5b1dc271 100644 --- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js +++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js @@ -1,5 +1,5 @@ import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; -import { createTestEditor } from '../test_utils'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"> <code> @@ -12,34 +12,78 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language describe('content_editor/extensions/code_block_highlight', () => { let parsedCodeBlockHtmlFixture; let tiptapEditor; + let doc; + let codeBlock; + let languageLoader; const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html'); const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre'); beforeEach(() => { - tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] }); - parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML); + languageLoader = { loadLanguages: jest.fn() }; + tiptapEditor = createTestEditor({ + extensions: [CodeBlockHighlight.configure({ languageLoader })], + }); - tiptapEditor.commands.setContent(CODE_BLOCK_HTML); + ({ + builders: { doc, codeBlock }, + } = createDocBuilder({ + tiptapEditor, + names: { + codeBlock: { nodeType: CodeBlockHighlight.name }, + }, + })); }); - it('extracts language and params attributes from Markdown API output', () => { - const language = preElement().getAttribute('lang'); + describe('when parsing HTML', () => { + beforeEach(() => { + parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML); - expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({ - language, + tiptapEditor.commands.setContent(CODE_BLOCK_HTML); + }); + it('extracts language and params attributes from Markdown API output', () => { + const language = preElement().getAttribute('lang'); + + expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({ + language, + }); + }); + + it('adds code, highlight, and js-syntax-highlight to code block element', () => { + const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre'); + + expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight'); }); - }); - it('adds code, highlight, and js-syntax-highlight to code block element', () => { - const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre'); + it('adds content-editor-code-block class to the pre element', () => { + const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre'); - expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight'); + expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block'); + }); }); - it('adds content-editor-code-block class to the pre element', () => { - const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre'); + describe.each` + inputRule + ${'```'} + ${'~~~'} + `('when typing $inputRule input rule', ({ inputRule }) => { + const language = 'javascript'; + + beforeEach(() => { + triggerNodeInputRule({ + tiptapEditor, + inputRuleText: `${inputRule}${language} `, + }); + }); + + it('creates a new code block and loads related language', () => { + const expectedDoc = doc(codeBlock({ language })); - expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block'); + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + + it('loads language when language loader is available', () => { + expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]); + }); }); }); diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js new file mode 100644 index 00000000000..bb97c9afa41 --- /dev/null +++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js @@ -0,0 +1,70 @@ +import CodeBlockLanguageBlocker from '~/content_editor/services/code_block_language_loader'; + +describe('content_editor/services/code_block_language_loader', () => { + let languageLoader; + let lowlight; + + beforeEach(() => { + lowlight = { + languages: [], + registerLanguage: jest + .fn() + .mockImplementation((language) => lowlight.languages.push(language)), + registered: jest.fn().mockImplementation((language) => lowlight.languages.includes(language)), + }; + languageLoader = new CodeBlockLanguageBlocker(lowlight); + }); + + describe('loadLanguages', () => { + it('loads highlight.js language packages identified by a list of languages', async () => { + const languages = ['javascript', 'ruby']; + + await languageLoader.loadLanguages(languages); + + languages.forEach((language) => { + expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function)); + }); + }); + + describe('when language is already registered', () => { + it('does not load the language again', async () => { + const languages = ['javascript']; + + await languageLoader.loadLanguages(languages); + await languageLoader.loadLanguages(languages); + + expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('loadLanguagesFromDOM', () => { + it('loads highlight.js language packages identified by pre tags in a DOM fragment', async () => { + const parser = new DOMParser(); + const { body } = parser.parseFromString( + ` + <pre lang="javascript"></pre> + <pre lang="ruby"></pre> + `, + 'text/html', + ); + + await languageLoader.loadLanguagesFromDOM(body); + + expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function)); + expect(lowlight.registerLanguage).toHaveBeenCalledWith('ruby', expect.any(Function)); + }); + }); + + describe('isLanguageLoaded', () => { + it('returns true when a language is registered', async () => { + const language = 'javascript'; + + expect(languageLoader.isLanguageLoaded(language)).toBe(false); + + await languageLoader.loadLanguages([language]); + + expect(languageLoader.isLanguageLoaded(language)).toBe(true); + }); + }); +}); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index 3bc72b13302..5b7a27b501d 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -11,6 +11,7 @@ describe('content_editor/services/content_editor', () => { let contentEditor; let serializer; let deserializer; + let languageLoader; let eventHub; let doc; let p; @@ -27,8 +28,15 @@ describe('content_editor/services/content_editor', () => { serializer = { deserialize: jest.fn() }; deserializer = { deserialize: jest.fn() }; + languageLoader = { loadLanguagesFromDOM: jest.fn() }; eventHub = eventHubFactory(); - contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub }); + contentEditor = new ContentEditor({ + tiptapEditor, + serializer, + deserializer, + eventHub, + languageLoader, + }); }); describe('.dispose', () => { @@ -43,10 +51,12 @@ describe('content_editor/services/content_editor', () => { describe('when setSerializedContent succeeds', () => { let document; + const dom = {}; + const testMarkdown = '**bold text**'; beforeEach(() => { document = doc(p('document')); - deserializer.deserialize.mockResolvedValueOnce({ document }); + deserializer.deserialize.mockResolvedValueOnce({ document, dom }); }); it('emits loadingContent and loadingSuccess event in the eventHub', () => { @@ -59,14 +69,20 @@ describe('content_editor/services/content_editor', () => { expect(loadingContentEmitted).toBe(true); }); - contentEditor.setSerializedContent('**bold text**'); + contentEditor.setSerializedContent(testMarkdown); }); it('sets the deserialized document in the tiptap editor object', async () => { - await contentEditor.setSerializedContent('**bold text**'); + await contentEditor.setSerializedContent(testMarkdown); expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); }); + + it('passes deserialized DOM document to language loader', async () => { + await contentEditor.setSerializedContent(testMarkdown); + + expect(languageLoader.loadLanguagesFromDOM).toHaveBeenCalledWith(dom); + }); }); describe('when setSerializedContent fails', () => { diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index 1eae854eca3..cb4eb43b88d 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -140,7 +140,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: query_name = 'ready_to_merge.query.graphql' it "#{base_output_path}#{query_name}.json" do - query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: true) + query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?) post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s }) diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index db56f77b60e..b8c1bef0ddd 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -12,6 +12,7 @@ import { TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, } from '~/security_configuration/constants'; +import { TEMP_PROVIDER_URLS } from '~/security_configuration/components/constants'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/cache_utils'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; @@ -145,55 +146,60 @@ describe('TrainingProviderList component', () => { expect(findCards()).toHaveLength(TEST_TRAINING_PROVIDERS_DEFAULT.data.length); }); - TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach( - ({ name, description, url, isEnabled }, index) => { - it(`shows the name for card ${index}`, () => { - expect(findCards().at(index).text()).toContain(name); - }); + TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach(({ name, description, isEnabled }, index) => { + it(`shows the name for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(name); + }); - it(`shows the description for card ${index}`, () => { - expect(findCards().at(index).text()).toContain(description); - }); + it(`shows the description for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(description); + }); + + it(`shows the learn more link for enabled card ${index}`, () => { + const learnMoreLink = findCards().at(index).find(GlLink); + const tempLogo = TEMP_PROVIDER_URLS[name]; - it(`shows the learn more link for card ${index}`, () => { - expect(findLinks().at(index).attributes()).toEqual({ + if (tempLogo) { + expect(learnMoreLink.attributes()).toEqual({ target: '_blank', - href: url, + href: TEMP_PROVIDER_URLS[name], }); - }); + } else { + expect(learnMoreLink.exists()).toBe(false); + } + }); - it(`shows the toggle with the correct value for card ${index}`, () => { - expect(findToggles().at(index).props('value')).toEqual(isEnabled); - }); + it(`shows the toggle with the correct value for card ${index}`, () => { + expect(findToggles().at(index).props('value')).toEqual(isEnabled); + }); - it(`shows a radio button to select the provider as primary within card ${index}`, () => { - const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index); + it(`shows a radio button to select the provider as primary within card ${index}`, () => { + const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index); - // if the given provider is not enabled it should not be possible select it as primary - expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe( - isEnabled ? undefined : 'disabled', - ); + // if the given provider is not enabled it should not be possible select it as primary + expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe( + isEnabled ? undefined : 'disabled', + ); - expect(primaryProviderRadioForCurrentCard.text()).toBe( - TrainingProviderList.i18n.primaryTraining, - ); - }); + expect(primaryProviderRadioForCurrentCard.text()).toBe( + TrainingProviderList.i18n.primaryTraining, + ); + }); - it('shows a info-tooltip that describes the purpose of a primary provider', () => { - const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon); - const tooltip = getBinding(infoIcon.element, 'gl-tooltip'); + it('shows a info-tooltip that describes the purpose of a primary provider', () => { + const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon); + const tooltip = getBinding(infoIcon.element, 'gl-tooltip'); - expect(infoIcon.props()).toMatchObject({ - name: 'information-o', - }); - expect(tooltip.value).toBe(TrainingProviderList.i18n.primaryTrainingDescription); + expect(infoIcon.props()).toMatchObject({ + name: 'information-o', }); + expect(tooltip.value).toBe(TrainingProviderList.i18n.primaryTrainingDescription); + }); - it('does not show loader when query is populated', () => { - expect(findLoader().exists()).toBe(false); - }); - }, - ); + it('does not show loader when query is populated', () => { + expect(findLoader().exists()).toBe(false); + }); + }); }); describe('provider logo', () => { diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index 55f5c20e45d..18a480bf082 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -1,6 +1,6 @@ export const testProjectPath = 'foo/bar'; export const testProviderIds = [101, 102, 103]; -export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3']; +export const testProviderName = ['Kontra', 'Secure Code Warrior', 'Other Vendor']; export const testTrainingUrls = [ 'https://www.vendornameone.com/url', 'https://www.vendornametwo.com/url', |