diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-14 15:09:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-14 15:09:51 +0300 |
commit | 9223573b85bcfdd21953f52e0d2c5cb587e366a1 (patch) | |
tree | 7dfd09536b948d560fc442014a95a221327b6567 /spec/frontend/vue_shared | |
parent | 1fc72cb8765dab466da8555b70eb744a53a74a80 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap (renamed from spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap) | 0 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js | 121 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js | 84 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js | 93 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js | 192 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js | 45 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js | 173 |
7 files changed, 354 insertions, 354 deletions
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap index 26c9a6f8d5a..26c9a6f8d5a 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap +++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js deleted file mode 100644 index 395ba92d4c6..00000000000 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js +++ /dev/null @@ -1,121 +0,0 @@ -import { nextTick } from 'vue'; -import { GlIntersectionObserver } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue'; -import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; -import LineHighlighter from '~/blob/line_highlighter'; - -const lineHighlighter = new LineHighlighter(); -jest.mock('~/blob/line_highlighter', () => - jest.fn().mockReturnValue({ - highlightHash: jest.fn(), - }), -); - -const DEFAULT_PROPS = { - chunkIndex: 2, - isHighlighted: false, - content: '// Line 1 content \n // Line 2 content', - startingFrom: 140, - totalLines: 50, - language: 'javascript', - blamePath: 'blame/file.js', -}; - -const hash = '#L142'; - -describe('Chunk component', () => { - let wrapper; - let idleCallbackSpy; - - const createComponent = (props = {}) => { - wrapper = shallowMountExtended(Chunk, { - mocks: { $route: { hash } }, - propsData: { ...DEFAULT_PROPS, ...props }, - }); - }; - - const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); - const findChunkLines = () => wrapper.findAllComponents(ChunkLine); - const findLineNumbers = () => wrapper.findAllByTestId('line-number'); - const findContent = () => wrapper.findByTestId('content'); - - beforeEach(() => { - idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn()); - createComponent(); - }); - - describe('Intersection observer', () => { - it('renders an Intersection observer component', () => { - expect(findIntersectionObserver().exists()).toBe(true); - }); - - it('emits an appear event when intersection-observer appears', () => { - findIntersectionObserver().vm.$emit('appear'); - - expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]); - }); - - it('does not emit an appear event is isHighlighted is true', () => { - createComponent({ isHighlighted: true }); - findIntersectionObserver().vm.$emit('appear'); - - expect(wrapper.emitted('appear')).toEqual(undefined); - }); - }); - - describe('rendering', () => { - it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => { - jest.clearAllMocks(); - createComponent({ isFirstChunk: true }); - - expect(window.requestIdleCallback).not.toHaveBeenCalled(); - expect(findContent().exists()).toBe(true); - }); - - it('does not render a Chunk Line component if isHighlighted is false', () => { - expect(findChunkLines().length).toBe(0); - }); - - it('does not render simplified line numbers and content if browser is not in idle state', () => { - idleCallbackSpy.mockRestore(); - createComponent(); - - expect(findLineNumbers()).toHaveLength(0); - expect(findContent().exists()).toBe(false); - }); - - it('renders simplified line numbers and content if isHighlighted is false', () => { - expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines); - - expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`); - - expect(findContent().text()).toBe(DEFAULT_PROPS.content); - }); - - it('renders Chunk Line components if isHighlighted is true', () => { - const splitContent = DEFAULT_PROPS.content.split('\n'); - createComponent({ isHighlighted: true }); - - expect(findChunkLines().length).toBe(splitContent.length); - - expect(findChunkLines().at(0).props()).toMatchObject({ - number: DEFAULT_PROPS.startingFrom + 1, - content: splitContent[0], - language: DEFAULT_PROPS.language, - blamePath: DEFAULT_PROPS.blamePath, - }); - }); - - it('does not scroll to route hash if last chunk is not loaded', () => { - expect(LineHighlighter).not.toHaveBeenCalled(); - }); - - it('scrolls to route hash if last chunk is loaded', async () => { - createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 }); - await nextTick(); - expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); - expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js new file mode 100644 index 00000000000..919abc26e05 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js @@ -0,0 +1,84 @@ +import { nextTick } from 'vue'; +import { GlIntersectionObserver } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue'; +import { CHUNK_1, CHUNK_2 } from '../mock_data'; + +describe('Chunk component', () => { + let wrapper; + let idleCallbackSpy; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(Chunk, { + propsData: { ...CHUNK_1, ...props }, + }); + }; + + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const findLineNumbers = () => wrapper.findAllByTestId('line-numbers'); + const findContent = () => wrapper.findByTestId('content'); + + beforeEach(() => { + idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn()); + createComponent(); + }); + + describe('Intersection observer', () => { + it('renders an Intersection observer component', () => { + expect(findIntersectionObserver().exists()).toBe(true); + }); + + it('renders highlighted content if appear event is emitted', async () => { + createComponent({ chunkIndex: 1, isHighlighted: false }); + findIntersectionObserver().vm.$emit('appear'); + + await nextTick(); + + expect(findContent().exists()).toBe(true); + }); + }); + + describe('rendering', () => { + it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => { + jest.clearAllMocks(); + + expect(window.requestIdleCallback).not.toHaveBeenCalled(); + expect(findContent().text()).toBe(CHUNK_1.highlightedContent); + }); + + it('does not render content if browser is not in idle state', () => { + idleCallbackSpy.mockRestore(); + createComponent({ chunkIndex: 1, ...CHUNK_2 }); + + expect(findLineNumbers()).toHaveLength(0); + expect(findContent().exists()).toBe(false); + }); + + describe('isHighlighted is false', () => { + beforeEach(() => createComponent(CHUNK_2)); + + it('does not render line numbers', () => { + expect(findLineNumbers()).toHaveLength(0); + }); + + it('renders raw content', () => { + expect(findContent().text()).toBe(CHUNK_2.rawContent); + }); + }); + + describe('isHighlighted is true', () => { + beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true })); + + it('renders line numbers', () => { + expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines); + + // Opted for a snapshot test here since the output is simple and verifies native HTML elements + expect(findLineNumbers().at(0).element).toMatchSnapshot(); + }); + + it('renders highlighted content', () => { + expect(findContent().text()).toBe(CHUNK_2.highlightedContent); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js index ff50326917f..9e43aa1d707 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -2,7 +2,27 @@ import { nextTick } from 'vue'; import { GlIntersectionObserver } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; -import { CHUNK_1, CHUNK_2 } from '../mock_data'; +import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; +import LineHighlighter from '~/blob/line_highlighter'; + +const lineHighlighter = new LineHighlighter(); +jest.mock('~/blob/line_highlighter', () => + jest.fn().mockReturnValue({ + highlightHash: jest.fn(), + }), +); + +const DEFAULT_PROPS = { + chunkIndex: 2, + isHighlighted: false, + content: '// Line 1 content \n // Line 2 content', + startingFrom: 140, + totalLines: 50, + language: 'javascript', + blamePath: 'blame/file.js', +}; + +const hash = '#L142'; describe('Chunk component', () => { let wrapper; @@ -10,12 +30,14 @@ describe('Chunk component', () => { const createComponent = (props = {}) => { wrapper = shallowMountExtended(Chunk, { - propsData: { ...CHUNK_1, ...props }, + mocks: { $route: { hash } }, + propsData: { ...DEFAULT_PROPS, ...props }, }); }; const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); - const findLineNumbers = () => wrapper.findAllByTestId('line-numbers'); + const findChunkLines = () => wrapper.findAllComponents(ChunkLine); + const findLineNumbers = () => wrapper.findAllByTestId('line-number'); const findContent = () => wrapper.findByTestId('content'); beforeEach(() => { @@ -28,57 +50,72 @@ describe('Chunk component', () => { expect(findIntersectionObserver().exists()).toBe(true); }); - it('renders highlighted content if appear event is emitted', async () => { - createComponent({ chunkIndex: 1, isHighlighted: false }); + it('emits an appear event when intersection-observer appears', () => { findIntersectionObserver().vm.$emit('appear'); - await nextTick(); + expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]); + }); - expect(findContent().exists()).toBe(true); + it('does not emit an appear event is isHighlighted is true', () => { + createComponent({ isHighlighted: true }); + findIntersectionObserver().vm.$emit('appear'); + + expect(wrapper.emitted('appear')).toEqual(undefined); }); }); describe('rendering', () => { - it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => { + it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => { jest.clearAllMocks(); + createComponent({ isFirstChunk: true }); expect(window.requestIdleCallback).not.toHaveBeenCalled(); - expect(findContent().text()).toBe(CHUNK_1.highlightedContent); + expect(findContent().exists()).toBe(true); + }); + + it('does not render a Chunk Line component if isHighlighted is false', () => { + expect(findChunkLines().length).toBe(0); }); - it('does not render content if browser is not in idle state', () => { + it('does not render simplified line numbers and content if browser is not in idle state', () => { idleCallbackSpy.mockRestore(); - createComponent({ chunkIndex: 1, ...CHUNK_2 }); + createComponent(); expect(findLineNumbers()).toHaveLength(0); expect(findContent().exists()).toBe(false); }); - describe('isHighlighted is false', () => { - beforeEach(() => createComponent(CHUNK_2)); + it('renders simplified line numbers and content if isHighlighted is false', () => { + expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines); - it('does not render line numbers', () => { - expect(findLineNumbers()).toHaveLength(0); - }); + expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`); - it('renders raw content', () => { - expect(findContent().text()).toBe(CHUNK_2.rawContent); - }); + expect(findContent().text()).toBe(DEFAULT_PROPS.content); }); - describe('isHighlighted is true', () => { - beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true })); + it('renders Chunk Line components if isHighlighted is true', () => { + const splitContent = DEFAULT_PROPS.content.split('\n'); + createComponent({ isHighlighted: true }); - it('renders line numbers', () => { - expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines); + expect(findChunkLines().length).toBe(splitContent.length); - // Opted for a snapshot test here since the output is simple and verifies native HTML elements - expect(findLineNumbers().at(0).element).toMatchSnapshot(); + expect(findChunkLines().at(0).props()).toMatchObject({ + number: DEFAULT_PROPS.startingFrom + 1, + content: splitContent[0], + language: DEFAULT_PROPS.language, + blamePath: DEFAULT_PROPS.blamePath, }); + }); - it('renders highlighted content', () => { - expect(findContent().text()).toBe(CHUNK_2.highlightedContent); - }); + it('does not scroll to route hash if last chunk is not loaded', () => { + expect(LineHighlighter).not.toHaveBeenCalled(); + }); + + it('scrolls to route hash if last chunk is loaded', async () => { + createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 }); + await nextTick(); + expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); + expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash); }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js deleted file mode 100644 index e46c30d7c49..00000000000 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js +++ /dev/null @@ -1,192 +0,0 @@ -import hljs from 'highlight.js/lib/core'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue'; -import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; -import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue'; -import { - EVENT_ACTION, - EVENT_LABEL_VIEWER, - EVENT_LABEL_FALLBACK, - ROUGE_TO_HLJS_LANGUAGE_MAP, - LINES_PER_CHUNK, - LEGACY_FALLBACKS, - CODEOWNERS_FILE_NAME, - CODEOWNERS_LANGUAGE, -} from '~/vue_shared/components/source_viewer/constants'; -import waitForPromises from 'helpers/wait_for_promises'; -import LineHighlighter from '~/blob/line_highlighter'; -import eventHub from '~/notes/event_hub'; -import Tracking from '~/tracking'; - -jest.mock('~/blob/line_highlighter'); -jest.mock('highlight.js/lib/core'); -jest.mock('~/vue_shared/components/source_viewer/plugins/index'); -Vue.use(VueRouter); -const router = new VueRouter(); - -const generateContent = (content, totalLines = 1, delimiter = '\n') => { - let generatedContent = ''; - for (let i = 0; i < totalLines; i += 1) { - generatedContent += `Line: ${i + 1} = ${content}${delimiter}`; - } - return generatedContent; -}; - -const execImmediately = (callback) => callback(); - -describe('Source Viewer component', () => { - let wrapper; - const language = 'docker'; - const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; - const chunk1 = generateContent('// Some source code 1', 70); - const chunk2 = generateContent('// Some source code 2', 70); - const chunk3 = generateContent('// Some source code 3', 70, '\r\n'); - const chunk3Result = generateContent('// Some source code 3', 70, '\n'); - const content = chunk1 + chunk2 + chunk3; - const path = 'some/path.js'; - const blamePath = 'some/blame/path.js'; - const fileType = 'javascript'; - const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType }; - const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; - - const createComponent = async (blob = {}) => { - wrapper = shallowMountExtended(SourceViewer, { - router, - propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } }, - }); - await waitForPromises(); - }; - - const findChunks = () => wrapper.findAllComponents(Chunk); - - beforeEach(() => { - hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); - hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); - jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); - jest.spyOn(eventHub, '$emit'); - jest.spyOn(Tracking, 'event'); - - return createComponent(); - }); - - describe('event tracking', () => { - it('fires a tracking event when the component is created', () => { - const eventData = { label: EVENT_LABEL_VIEWER, property: language }; - expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); - }); - - it('does not emit an error event when the language is supported', () => { - expect(wrapper.emitted('error')).toBeUndefined(); - }); - - it('fires a tracking event and emits an error when the language is not supported', () => { - const unsupportedLanguage = 'apex'; - const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage }; - createComponent({ language: unsupportedLanguage }); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); - expect(wrapper.emitted('error')).toHaveLength(1); - }); - }); - - describe('legacy fallbacks', () => { - it.each(LEGACY_FALLBACKS)( - 'tracks a fallback event and emits an error when viewing %s files', - (fallbackLanguage) => { - const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage }; - createComponent({ language: fallbackLanguage }); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); - expect(wrapper.emitted('error')).toHaveLength(1); - }, - ); - }); - - describe('highlight.js', () => { - beforeEach(() => createComponent({ language: mappedLanguage })); - - it('registers our plugins for Highlight.js', () => { - expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content); - }); - - it('registers the language definition', async () => { - const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); - - expect(hljs.registerLanguage).toHaveBeenCalledWith( - mappedLanguage, - languageDefinition.default, - ); - }); - - it('registers json language definition if fileType is package_json', async () => { - await createComponent({ language: 'json', fileType: 'package_json' }); - const languageDefinition = await import(`highlight.js/lib/languages/json`); - - expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default); - }); - - it('correctly maps languages starting with uppercase', async () => { - await createComponent({ language: 'Ruby' }); - const languageDefinition = await import(`highlight.js/lib/languages/ruby`); - - expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default); - }); - - it('registers codeowners language definition if file name is CODEOWNERS', async () => { - await createComponent({ name: CODEOWNERS_FILE_NAME }); - const languageDefinition = await import( - '~/vue_shared/components/source_viewer/languages/codeowners' - ); - - expect(hljs.registerLanguage).toHaveBeenCalledWith( - CODEOWNERS_LANGUAGE, - languageDefinition.default, - ); - }); - - it('highlights the first chunk', () => { - expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); - expect(findChunks().at(0).props('isFirstChunk')).toBe(true); - }); - - describe('auto-detects if a language cannot be loaded', () => { - beforeEach(() => createComponent({ language: 'some_unknown_language' })); - - it('highlights the content with auto-detection', () => { - expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim()); - }); - }); - }); - - describe('rendering', () => { - it.each` - chunkIndex | chunkContent | totalChunks - ${0} | ${chunk1} | ${0} - ${1} | ${chunk2} | ${3} - ${2} | ${chunk3Result} | ${3} - `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => { - const chunk = findChunks().at(chunkIndex); - - expect(chunk.props('content')).toContain(chunkContent.trim()); - - expect(chunk.props()).toMatchObject({ - totalLines: LINES_PER_CHUNK, - startingFrom: LINES_PER_CHUNK * chunkIndex, - totalChunks, - }); - }); - - it('emits showBlobInteractionZones on the eventHub when chunk appears', () => { - findChunks().at(0).vm.$emit('appear'); - expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path); - }); - }); - - describe('LineHighlighter', () => { - it('instantiates the lineHighlighter class', () => { - expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js new file mode 100644 index 00000000000..715234e56fd --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js @@ -0,0 +1,45 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue'; +import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue'; +import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants'; +import Tracking from '~/tracking'; +import addBlobLinksTracking from '~/blob/blob_links_tracking'; +import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data'; + +jest.mock('~/blob/blob_links_tracking'); + +describe('Source Viewer component', () => { + let wrapper; + const CHUNKS_MOCK = [CHUNK_1, CHUNK_2]; + + const createComponent = () => { + wrapper = shallowMountExtended(SourceViewer, { + propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK }, + }); + }; + + const findChunks = () => wrapper.findAllComponents(Chunk); + + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + return createComponent(); + }); + + describe('event tracking', () => { + it('fires a tracking event when the component is created', () => { + const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK }; + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + }); + + it('adds blob links tracking', () => { + expect(addBlobLinksTracking).toHaveBeenCalled(); + }); + }); + + describe('rendering', () => { + it('renders a Chunk component for each chunk', () => { + expect(findChunks().at(0).props()).toMatchObject(CHUNK_1); + expect(findChunks().at(1).props()).toMatchObject(CHUNK_2); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 46b582c3668..6b1d65c5a6a 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -1,45 +1,192 @@ +import hljs from 'highlight.js/lib/core'; +import Vue from 'vue'; +import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; +import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; -import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants'; +import { + EVENT_ACTION, + EVENT_LABEL_VIEWER, + EVENT_LABEL_FALLBACK, + ROUGE_TO_HLJS_LANGUAGE_MAP, + LINES_PER_CHUNK, + LEGACY_FALLBACKS, + CODEOWNERS_FILE_NAME, + CODEOWNERS_LANGUAGE, +} from '~/vue_shared/components/source_viewer/constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import LineHighlighter from '~/blob/line_highlighter'; +import eventHub from '~/notes/event_hub'; import Tracking from '~/tracking'; -import addBlobLinksTracking from '~/blob/blob_links_tracking'; -import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data'; -jest.mock('~/blob/blob_links_tracking'); +jest.mock('~/blob/line_highlighter'); +jest.mock('highlight.js/lib/core'); +jest.mock('~/vue_shared/components/source_viewer/plugins/index'); +Vue.use(VueRouter); +const router = new VueRouter(); + +const generateContent = (content, totalLines = 1, delimiter = '\n') => { + let generatedContent = ''; + for (let i = 0; i < totalLines; i += 1) { + generatedContent += `Line: ${i + 1} = ${content}${delimiter}`; + } + return generatedContent; +}; + +const execImmediately = (callback) => callback(); describe('Source Viewer component', () => { let wrapper; - const CHUNKS_MOCK = [CHUNK_1, CHUNK_2]; + const language = 'docker'; + const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; + const chunk1 = generateContent('// Some source code 1', 70); + const chunk2 = generateContent('// Some source code 2', 70); + const chunk3 = generateContent('// Some source code 3', 70, '\r\n'); + const chunk3Result = generateContent('// Some source code 3', 70, '\n'); + const content = chunk1 + chunk2 + chunk3; + const path = 'some/path.js'; + const blamePath = 'some/blame/path.js'; + const fileType = 'javascript'; + const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType }; + const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; - const createComponent = () => { + const createComponent = async (blob = {}) => { wrapper = shallowMountExtended(SourceViewer, { - propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK }, + router, + propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } }, }); + await waitForPromises(); }; const findChunks = () => wrapper.findAllComponents(Chunk); beforeEach(() => { + hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); + hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); + jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); + jest.spyOn(eventHub, '$emit'); jest.spyOn(Tracking, 'event'); + return createComponent(); }); describe('event tracking', () => { it('fires a tracking event when the component is created', () => { - const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK }; + const eventData = { label: EVENT_LABEL_VIEWER, property: language }; + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + }); + + it('does not emit an error event when the language is supported', () => { + expect(wrapper.emitted('error')).toBeUndefined(); + }); + + it('fires a tracking event and emits an error when the language is not supported', () => { + const unsupportedLanguage = 'apex'; + const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage }; + createComponent({ language: unsupportedLanguage }); + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); + + describe('legacy fallbacks', () => { + it.each(LEGACY_FALLBACKS)( + 'tracks a fallback event and emits an error when viewing %s files', + (fallbackLanguage) => { + const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage }; + createComponent({ language: fallbackLanguage }); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + expect(wrapper.emitted('error')).toHaveLength(1); + }, + ); + }); + + describe('highlight.js', () => { + beforeEach(() => createComponent({ language: mappedLanguage })); + + it('registers our plugins for Highlight.js', () => { + expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content); + }); + + it('registers the language definition', async () => { + const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith( + mappedLanguage, + languageDefinition.default, + ); }); - it('adds blob links tracking', () => { - expect(addBlobLinksTracking).toHaveBeenCalled(); + it('registers json language definition if fileType is package_json', async () => { + await createComponent({ language: 'json', fileType: 'package_json' }); + const languageDefinition = await import(`highlight.js/lib/languages/json`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default); + }); + + it('correctly maps languages starting with uppercase', async () => { + await createComponent({ language: 'Ruby' }); + const languageDefinition = await import(`highlight.js/lib/languages/ruby`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default); + }); + + it('registers codeowners language definition if file name is CODEOWNERS', async () => { + await createComponent({ name: CODEOWNERS_FILE_NAME }); + const languageDefinition = await import( + '~/vue_shared/components/source_viewer/languages/codeowners' + ); + + expect(hljs.registerLanguage).toHaveBeenCalledWith( + CODEOWNERS_LANGUAGE, + languageDefinition.default, + ); + }); + + it('highlights the first chunk', () => { + expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); + expect(findChunks().at(0).props('isFirstChunk')).toBe(true); + }); + + describe('auto-detects if a language cannot be loaded', () => { + beforeEach(() => createComponent({ language: 'some_unknown_language' })); + + it('highlights the content with auto-detection', () => { + expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim()); + }); }); }); describe('rendering', () => { - it('renders a Chunk component for each chunk', () => { - expect(findChunks().at(0).props()).toMatchObject(CHUNK_1); - expect(findChunks().at(1).props()).toMatchObject(CHUNK_2); + it.each` + chunkIndex | chunkContent | totalChunks + ${0} | ${chunk1} | ${0} + ${1} | ${chunk2} | ${3} + ${2} | ${chunk3Result} | ${3} + `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => { + const chunk = findChunks().at(chunkIndex); + + expect(chunk.props('content')).toContain(chunkContent.trim()); + + expect(chunk.props()).toMatchObject({ + totalLines: LINES_PER_CHUNK, + startingFrom: LINES_PER_CHUNK * chunkIndex, + totalChunks, + }); + }); + + it('emits showBlobInteractionZones on the eventHub when chunk appears', () => { + findChunks().at(0).vm.$emit('appear'); + expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path); + }); + }); + + describe('LineHighlighter', () => { + it('instantiates the lineHighlighter class', () => { + expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); }); }); }); |