From 27279ec872f828ab54f27a3fae17eef5a14e4d4f Mon Sep 17 00:00:00 2001 From: Jacques Date: Mon, 14 Feb 2022 16:43:45 +0100 Subject: [PATCH 1/2] Improve the performance of highlight.js Improved the performance by highlighting in chunks --- .../javascripts/blob/line_highlighter.js | 2 + .../vue_shared/components/line_numbers.vue | 31 ---- .../source_viewer/components/chunk.vue | 103 +++++++++++ .../source_viewer/components/chunk_line.vue | 44 +++++ .../components/source_viewer/constants.js | 2 + .../source_viewer/source_viewer.vue | 168 ++++++++++++------ .../components/source_viewer/utils.js | 28 --- .../blobs/blob_line_permalink_updater_spec.rb | 8 +- .../components/line_numbers_spec.js | 37 ---- .../components/chunk_line_spec.js | 47 +++++ .../source_viewer/components/chunk_spec.js | 82 +++++++++ .../source_viewer/source_viewer_spec.js | 97 ++++------ .../components/source_viewer/utils_spec.js | 26 --- 13 files changed, 438 insertions(+), 237 deletions(-) delete mode 100644 app/assets/javascripts/vue_shared/components/line_numbers.vue create mode 100644 app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue create mode 100644 app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue delete mode 100644 app/assets/javascripts/vue_shared/components/source_viewer/utils.js delete mode 100644 spec/frontend/vue_shared/components/line_numbers_spec.js create mode 100644 spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js create mode 100644 spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js delete mode 100644 spec/frontend/vue_shared/components/source_viewer/utils_spec.js diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js index a1f59aa1b54978..a8932f8c73b753 100644 --- a/app/assets/javascripts/blob/line_highlighter.js +++ b/app/assets/javascripts/blob/line_highlighter.js @@ -37,6 +37,7 @@ const LineHighlighter = function (options = {}) { options.fileHolderSelector = options.fileHolderSelector || '.file-holder'; options.scrollFileHolder = options.scrollFileHolder || false; options.hash = options.hash || window.location.hash; + options.scrollBehavior = options.scrollBehavior || 'smooth'; this.options = options; this._hash = options.hash; @@ -74,6 +75,7 @@ LineHighlighter.prototype.highlightHash = function (newHash) { // Scroll to the first highlighted line on initial load // Add an offset of -100 for some context offset: -100, + behavior: this.options.scrollBehavior, }); } } diff --git a/app/assets/javascripts/vue_shared/components/line_numbers.vue b/app/assets/javascripts/vue_shared/components/line_numbers.vue deleted file mode 100644 index 11caf3be00ad34..00000000000000 --- a/app/assets/javascripts/vue_shared/components/line_numbers.vue +++ /dev/null @@ -1,31 +0,0 @@ - - diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue new file mode 100644 index 00000000000000..db0ee0cf00eba4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue @@ -0,0 +1,103 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue new file mode 100644 index 00000000000000..ab989ae6f9eb83 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue @@ -0,0 +1,44 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index 9efe0147c37f5c..b5ce214e5774b5 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -109,3 +109,5 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = { xquery: 'xquery', yaml: 'yaml', }; + +export const LINES_PER_CHUNK = 70; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 4a78cbacec0e67..f375aa2b63ffb6 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -1,16 +1,21 @@ diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js deleted file mode 100644 index d726a8a55ffdb6..00000000000000 --- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js +++ /dev/null @@ -1,28 +0,0 @@ -export const wrapLines = (content, language) => { - const isValidLanguage = /^[a-z\d\-_]+$/.test(language); // To prevent the possibility of a vulnerability we only allow languages that contain alphanumeric characters ([a-z\d), dashes (-) or underscores (_). - - return ( - content && - content - .split('\n') - .map((line, i) => { - let formattedLine; - const attributes = `id="LC${i + 1}" lang="${isValidLanguage ? language : ''}"`; - - if (line.includes('```bash - * example (after): ```bash - */ - formattedLine = line.replace(/(?=class="hljs)/, `${attributes} `); - } else { - formattedLine = `${line}`; - } - - return formattedLine; - }) - .join('\n') - ); -}; diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb index 11e2d24c36acc6..9b0edcd09d24f7 100644 --- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb +++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb @@ -39,7 +39,7 @@ def visit_blob(fragment = nil) find('#L3').click find("#L5").click - expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "LC5"))) + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "L5"))) end it 'with initial fragment hash, changes fragment hash if line number clicked' do @@ -50,7 +50,7 @@ def visit_blob(fragment = nil) find('#L3').click find("#L5").click - expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "LC5"))) + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "L5"))) end end @@ -75,7 +75,7 @@ def visit_blob(fragment = nil) find('#L3').click find("#L5").click - expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "LC5"))) + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "L5"))) end it 'with initial fragment hash, changes fragment hash if line number clicked' do @@ -86,7 +86,7 @@ def visit_blob(fragment = nil) find('#L3').click find("#L5").click - expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "LC5"))) + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "L5"))) end end end diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js deleted file mode 100644 index 38c2622686356e..00000000000000 --- a/spec/frontend/vue_shared/components/line_numbers_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon, GlLink } from '@gitlab/ui'; -import LineNumbers from '~/vue_shared/components/line_numbers.vue'; - -describe('Line Numbers component', () => { - let wrapper; - const lines = 10; - - const createComponent = () => { - wrapper = shallowMount(LineNumbers, { propsData: { lines } }); - }; - - const findGlIcon = () => wrapper.findComponent(GlIcon); - const findLineNumbers = () => wrapper.findAllComponents(GlLink); - const findFirstLineNumber = () => findLineNumbers().at(0); - - beforeEach(() => createComponent()); - - afterEach(() => wrapper.destroy()); - - describe('rendering', () => { - it('renders Line Numbers', () => { - expect(findLineNumbers().length).toBe(lines); - expect(findFirstLineNumber().attributes()).toMatchObject({ - id: 'L1', - to: '#LC1', - }); - }); - - it('renders a link icon', () => { - expect(findGlIcon().props()).toMatchObject({ - size: 12, - name: 'link', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js new file mode 100644 index 00000000000000..4aa0aea9605aa4 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js @@ -0,0 +1,47 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; + +const DEFAULT_PROPS = { + number: 2, + content: '// Line content', + language: 'javascript', +}; + +describe('Chunk Line component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } }); + }; + + const findLink = () => wrapper.findComponent(GlLink); + const findContent = () => wrapper.findByTestId('content'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => wrapper.destroy()); + + describe('rendering', () => { + it('renders a line number', () => { + expect(findLink().attributes()).toMatchObject({ + 'data-line-number': `${DEFAULT_PROPS.number}`, + to: `#L${DEFAULT_PROPS.number}`, + id: `L${DEFAULT_PROPS.number}`, + }); + + expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString()); + }); + + it('renders content', () => { + expect(findContent().attributes()).toMatchObject({ + id: `LC${DEFAULT_PROPS.number}`, + lang: DEFAULT_PROPS.language, + }); + + expect(findContent().text()).toBe(DEFAULT_PROPS.content); + }); + }); +}); 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 new file mode 100644 index 00000000000000..42c4f2eacb838f --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -0,0 +1,82 @@ +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 ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; + +const DEFAULT_PROPS = { + chunkIndex: 2, + isHighlighted: false, + content: '// Line 1 content \n // Line 2 content', + startingFrom: 140, + totalLines: 50, + language: 'javascript', +}; + +describe('Chunk component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(Chunk, { 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(() => { + createComponent(); + }); + + afterEach(() => wrapper.destroy()); + + 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 render a Chunk Line component if isHighlighted is false', () => { + expect(findChunkLines().length).toBe(0); + }); + + it('renders simplified line numbers and content if isHighlighted is false', () => { + expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines); + + expect(findLineNumbers().at(0).attributes()).toMatchObject({ + 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`, + href: `#L${DEFAULT_PROPS.startingFrom + 1}`, + id: `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, + }); + }); + }); +}); 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 ab579945e22649..138ded5fb86777 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,23 +1,35 @@ import hljs from 'highlight.js/lib/core'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; +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 Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; -import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils'; +import LineHighlighter from '~/blob/line_highlighter'; +jest.mock('~/blob/line_highlighter'); jest.mock('highlight.js/lib/core'); Vue.use(VueRouter); const router = new VueRouter(); +const generateContent = (content, totalLines = 1) => { + let generatedContent = ''; + for (let i = 0; i < totalLines; i += 1) { + generatedContent += `Line: ${i + 1} = ${content}\n`; + } + return generatedContent; +}; + +const execImmediately = (callback) => callback(); + describe('Source Viewer component', () => { let wrapper; const language = 'docker'; const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; - const content = `// Some source code`; + const chunk1 = generateContent('// Some source code 1', 70); + const chunk2 = generateContent('// Some source code 2', 70); + const content = chunk1 + chunk2; const DEFAULT_BLOB_DATA = { language, rawTextBlob: content }; const highlightedContent = `${content}`; @@ -29,15 +41,12 @@ describe('Source Viewer component', () => { await waitForPromises(); }; - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findLineNumbers = () => wrapper.findComponent(LineNumbers); - const findHighlightedContent = () => wrapper.findByTestId('test-highlighted'); - const findFirstLine = () => wrapper.find('#LC1'); + const findChunks = () => wrapper.findAllComponents(Chunk); beforeEach(() => { hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); - jest.spyOn(sourceViewerUtils, 'wrapLines'); + jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); return createComponent(); }); @@ -45,6 +54,8 @@ describe('Source Viewer component', () => { afterEach(() => wrapper.destroy()); describe('highlight.js', () => { + beforeEach(() => createComponent({ language: mappedLanguage })); + it('registers the language definition', async () => { const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); @@ -54,72 +65,38 @@ describe('Source Viewer component', () => { ); }); - it('highlights the content', () => { - expect(hljs.highlight).toHaveBeenCalledWith(content, { language: mappedLanguage }); + it('highlights the first chunk', () => { + expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); }); 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(content); + expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim()); }); }); }); describe('rendering', () => { - it('renders a loading icon if no highlighted content is available yet', async () => { - hljs.highlight.mockImplementation(() => ({ value: null })); - await createComponent(); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => { - expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage); - }); - - it('renders Line Numbers', () => { - expect(findLineNumbers().props('lines')).toBe(1); + it('renders the first chunk', async () => { + const firstChunk = findChunks().at(0); + expect(firstChunk.props('content')).toContain(chunk1); + expect(firstChunk.props('totalLines')).toBe(70); + expect(firstChunk.props('startingFrom')).toBe(0); }); - it('renders the highlighted content', () => { - expect(findHighlightedContent().exists()).toBe(true); + it('renders the second chunk', async () => { + const secondChunk = findChunks().at(1); + expect(secondChunk.props('content')).toContain(chunk2.trim()); + expect(secondChunk.props('totalLines')).toBe(70); + expect(secondChunk.props('startingFrom')).toBe(70); }); }); - describe('selecting a line', () => { - let firstLine; - let firstLineElement; - - beforeEach(() => { - firstLine = findFirstLine(); - firstLineElement = firstLine.element; - - jest.spyOn(firstLineElement, 'scrollIntoView'); - jest.spyOn(firstLineElement.classList, 'add'); - jest.spyOn(firstLineElement.classList, 'remove'); - }); - - it('adds the highlight (hll) class', async () => { - wrapper.vm.$router.push('#LC1'); - await nextTick(); - - expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll'); - }); - - it('removes the highlight (hll) class from a previously highlighted line', async () => { - wrapper.vm.$router.push('#LC2'); - await nextTick(); - - expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll'); - }); - - it('scrolls the line into view', () => { - expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({ - behavior: 'smooth', - block: 'center', - }); + describe('LineHighlighter', () => { + it('instantiates the lineHighlighter class', async () => { + expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js deleted file mode 100644 index 0631e7efd5418c..00000000000000 --- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js +++ /dev/null @@ -1,26 +0,0 @@ -import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; - -describe('Wrap lines', () => { - it.each` - content | language | output - ${'line 1'} | ${'javascript'} | ${'line 1'} - ${'line 1\nline 2'} | ${'html'} | ${`line 1\nline 2`} - ${'line 1\nline 2'} | ${'html'} | ${`line 1\nline 2`} - ${'```bash'} | ${'bash'} | ${'```bash'} - ${'```bash'} | ${'valid-language1'} | ${'```bash'} - ${'```bash'} | ${'valid_language2'} | ${'```bash'} - `('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => { - expect(wrapLines(content, language)).toBe(output); - }); - - it.each` - language - ${'invalidLanguage>'} - ${'"invalidLanguage"'} - ${' { - expect(wrapLines('```bash', language)).toBe( - '```bash', - ); - }); -}); -- GitLab From 64be3a302b0ba60ac92641570ebbe370dcd396b4 Mon Sep 17 00:00:00 2001 From: Jacques Date: Mon, 21 Mar 2022 14:04:09 +0100 Subject: [PATCH 2/2] Address maintainer review comments Addressed general code review comments --- .../source_viewer/source_viewer.vue | 33 +++++++++---------- .../source_viewer/source_viewer_spec.js | 16 ++++++--- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index f375aa2b63ffb6..15411e8d17e002 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -8,7 +8,7 @@ import Chunk from './components/chunk.vue'; * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible. * - * The rest of the lines (L70+) is rendered once the browser goes into an idle state (requestIdleCallback). + * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback). * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes, * it does not trigger a repaint on a parent element that wraps all 1000 lines. */ @@ -99,9 +99,7 @@ export default { const { highlightedContent, language } = this.highlight(chunk.content, this.language); - chunk.content = highlightedContent; - chunk.language = language; - chunk.isHighlighted = true; + Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true }); this.selectLine(); }, @@ -168,19 +166,18 @@ export default { /> -
- -
+ 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 138ded5fb86777..5b787993492a9d 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 @@ -81,16 +81,24 @@ describe('Source Viewer component', () => { describe('rendering', () => { it('renders the first chunk', async () => { const firstChunk = findChunks().at(0); + expect(firstChunk.props('content')).toContain(chunk1); - expect(firstChunk.props('totalLines')).toBe(70); - expect(firstChunk.props('startingFrom')).toBe(0); + + expect(firstChunk.props()).toMatchObject({ + totalLines: 70, + startingFrom: 0, + }); }); it('renders the second chunk', async () => { const secondChunk = findChunks().at(1); + expect(secondChunk.props('content')).toContain(chunk2.trim()); - expect(secondChunk.props('totalLines')).toBe(70); - expect(secondChunk.props('startingFrom')).toBe(70); + + expect(secondChunk.props()).toMatchObject({ + totalLines: 70, + startingFrom: 70, + }); }); }); -- GitLab