import { shallowMount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils'; import highlightMixin from '~/repository/mixins/highlight_mixin'; import LineHighlighter from '~/blob/line_highlighter'; import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { TEXT_FILE_TYPE } from '~/repository/constants'; import { LINES_PER_CHUNK, EVENT_ACTION, EVENT_LABEL_FALLBACK, } from '~/vue_shared/components/source_viewer/constants'; import Tracking from '~/tracking'; const lineHighlighter = new LineHighlighter(); jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() })); jest.mock('~/vue_shared/components/source_viewer/workers/highlight_utils', () => ({ splitIntoChunks: jest.fn().mockResolvedValue([]), })); const mockAxios = new MockAdapter(axios); const workerMock = { postMessage: jest.fn() }; const onErrorMock = jest.fn(); describe('HighlightMixin', () => { let wrapper; const hash = '#L50'; const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code const rawTextBlob = contentArray.join('\n'); const languageMock = 'json'; const createComponent = ( { fileType = TEXT_FILE_TYPE, language = languageMock, externalStorageUrl, rawPath } = {}, isUsingLfs = false, ) => { const simpleViewer = { fileType }; const dummyComponent = { mixins: [highlightMixin], inject: { highlightWorker: { default: workerMock }, glFeatures: { default: { highlightJsWorker: true } }, }, template: '
{{chunks[0]?.highlightedContent}}
', created() { this.initHighlightWorker( { rawTextBlob, simpleViewer, language, fileType, externalStorageUrl, rawPath }, isUsingLfs, ); }, methods: { onError: onErrorMock }, }; wrapper = shallowMount(dummyComponent, { mocks: { $route: { hash } } }); }; beforeEach(() => createComponent()); describe('initHighlightWorker', () => { const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n'); it('generates a chunk for the first 70 lines of raw text', () => { expect(splitIntoChunks).toHaveBeenCalledWith(languageMock, firstSeventyLines); }); it('calls postMessage on the worker', () => { expect(workerMock.postMessage.mock.calls.length).toBe(2); // first call instructs worker to highlight the first 70 lines expect(workerMock.postMessage.mock.calls[0][0]).toMatchObject({ content: firstSeventyLines, language: languageMock, }); // second call instructs worker to highlight all of the lines expect(workerMock.postMessage.mock.calls[1][0]).toMatchObject({ content: rawTextBlob, language: languageMock, fileType: TEXT_FILE_TYPE, }); }); }); describe('auto-detects if a language cannot be loaded', () => { const unknownLanguage = 'some_unknown_language'; beforeEach(() => { jest.spyOn(Tracking, 'event'); createComponent({ language: unknownLanguage }); }); it('emits a tracking event for the fallback', () => { const eventData = { label: EVENT_LABEL_FALLBACK, property: unknownLanguage }; expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); }); it('calls the onError method', () => { expect(onErrorMock).toHaveBeenCalled(); }); }); describe('worker message handling', () => { const CHUNK_MOCK = { startingFrom: 0, totalLines: 70, highlightedContent: 'some content' }; beforeEach(() => workerMock.onmessage({ data: [CHUNK_MOCK] })); it('updates the chunks data', () => { expect(wrapper.text()).toBe(CHUNK_MOCK.highlightedContent); }); it('highlights hash', () => { expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash); }); }); describe('LFS blobs', () => { const rawPath = '/org/project/-/raw/file.xml'; const externalStorageUrl = 'http://127.0.0.1:9000/lfs-objects/91/12/1341234'; const mockParams = { content: rawTextBlob, language: languageMock, fileType: TEXT_FILE_TYPE }; afterEach(() => mockAxios.reset()); it('Uses externalStorageUrl to fetch content if present', async () => { mockAxios.onGet(externalStorageUrl).replyOnce(HTTP_STATUS_OK, rawTextBlob); createComponent({ rawPath, externalStorageUrl }, true); await waitForPromises(); expect(mockAxios.history.get).toHaveLength(1); expect(mockAxios.history.get[0].url).toBe(externalStorageUrl); expect(workerMock.postMessage).toHaveBeenCalledWith(mockParams); }); it('Falls back to rawPath to fetch content', async () => { mockAxios.onGet(rawPath).replyOnce(HTTP_STATUS_OK, rawTextBlob); createComponent({ rawPath }, true); await waitForPromises(); expect(mockAxios.history.get).toHaveLength(1); expect(mockAxios.history.get[0].url).toBe(rawPath); expect(workerMock.postMessage).toHaveBeenCalledWith(mockParams); }); }); });