Skip to content

Commit

Permalink
Merge pull request from GHSA-rvgm-35jw-q628
Browse files Browse the repository at this point in the history
  • Loading branch information
sjwall authored Aug 22, 2022
1 parent 138147d commit f2b9938
Show file tree
Hide file tree
Showing 7 changed files with 710 additions and 257 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-typescript": "^8.3.4",
"@testing-library/react": "^11.1.0",
"@types/jest": "^27.4.0",
"@types/mermaid": "^8.2.7",
"@types/react": "^17.0.38",
Expand All @@ -69,7 +70,7 @@
"jest": "^27.4.7",
"mermaid": "^8.0.0",
"react": "^17.0.1",
"react-test-renderer": "^17.0.2",
"react-dom": "^17.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.78.1",
"ts-jest": "^27.1.2",
Expand Down
244 changes: 92 additions & 152 deletions src/Mermaid.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
* This source code is licensed under the MIT license found in the
* license file in the root directory of this source tree.
*/
import mermaid from 'mermaid'
import React from 'react'
import renderer from 'react-test-renderer'
import { act, render, RenderResult } from '@testing-library/react'
import { Mermaid } from './Mermaid'
import {
DARK_THEME_KEY,
Expand All @@ -18,185 +17,126 @@ import {
} from './theme.helper'
import * as ThemeHelper from './theme.helper'

async function waitFor (ms: number) {
return new Promise<void>(resolve => {
setTimeout(() => resolve(), ms)
})
}

jest.mock('mermaid')

// eslint-disable-next-line import/first
import mermaid from 'mermaid'

const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme')

const diagram = `graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`

afterEach(() => {
jest.clearAllMocks()
})

const removeUniqueness = (element: Element) => {
element.querySelectorAll('style').forEach((v) => v.remove())
element.querySelectorAll('svg').forEach((v) => {
v.removeAttribute('id')
v.parentElement!.removeAttribute('id')
})
}

const expectMermaidMatch = (result: RenderResult) => {
removeUniqueness(result.baseElement)
expect(result.baseElement.parentElement).toMatchSnapshot()
return result
}

it('renders without diagram', () => {
const component = renderer.create(<Mermaid chart={''} config={{}} />)
expect(mermaid.initialize).toBeCalledTimes(0)
expect(mermaid.render).toBeCalledTimes(0)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
expectMermaidMatch(render(<Mermaid chart={''} config={{}} />))
})

it('renders with diagram', () => {
const component = renderer.create(<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />)
expect(mermaid.initialize).toBeCalledTimes(0)
expect(mermaid.render).toBeCalledTimes(0)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
expectMermaidMatch(render(<svg><Mermaid chart={diagram} config={{}} /></svg>))
})

it('initializes only once', async () => {
const component = renderer.create(<>
<Mermaid chart={'foo'} config={{}} />
<Mermaid chart={'bar'} />
</>)
expect(mermaid.initialize).toBeCalledTimes(0)
expect(mermaid.render).toBeCalledTimes(0)
await waitFor(1000)
component.update()
expect(mermaid.render).toBeCalledTimes(2)
it('renders with diagram change', () => {
const config = {}
jest.useFakeTimers()
const view = expectMermaidMatch(render(<Mermaid chart={diagram} config={config} />))
view.rerender(<Mermaid chart={`graph TD;
D-->C;
D-->B;
C-->A;
B-->A;`} config={config} />)
jest.advanceTimersByTime(1000)
expectMermaidMatch(view)
jest.useRealTimers()
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
component.update()
expect(mermaid.render).toBeCalledTimes(2)
})

it('initializes only once', () => {
expectMermaidMatch(render(<>
<Mermaid chart={'foo'} config={{}} />
<Mermaid chart={'bar'} />
</>))
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
})

it('renders with mermaid config', () => {
const component = renderer.create(<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{ mermaid: { theme: 'dark' } } } />)
expect(mermaid.initialize).toBeCalledTimes(0)
expect(mermaid.render).toBeCalledTimes(0)
component.update()
expect(mermaid.render).toHaveBeenCalled()
expect(mermaid.initialize).toHaveBeenNthCalledWith(1, { startOnLoad: true, theme: 'dark' })
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
expectMermaidMatch(render(<Mermaid chart={diagram} config={{ mermaid: { theme: 'dark' } }} />))
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' })
})

it('re-renders mermaid theme on html data-theme attribute change', async () => {
const component = renderer.create(
<html data-theme='light'>
<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />
</html>)
expect(mermaid.initialize).toBeCalledTimes(0)
expect(mermaid.render).toBeCalledTimes(0)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
component.update()
expect(mermaid.render).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
it('renders with mermaid config change', () => {
const view = expectMermaidMatch(render(<Mermaid chart={diagram} config={{ mermaid: { theme: 'dark' } }} />))
view.baseElement.querySelectorAll('div.mermaid').forEach((v) => {
v.setAttribute('data-processed', 'true')
})
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' })
view.rerender(<Mermaid chart={diagram} config={{ mermaid: { theme: 'forest' } }} />)
// await waitFor(1000)
expectMermaidMatch(view)
expect(mermaid.contentLoaded).toBeCalledTimes(2)
expect(mermaid.initialize).toHaveBeenNthCalledWith(2, { startOnLoad: true, theme: 'forest' })
})

component.update(
<html data-theme='dark'>
<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />
</html>)
it('renders with string mermaid config', () => {
expectMermaidMatch(render(<Mermaid chart={diagram} config={JSON.stringify({ mermaid: { theme: 'dark' } })} />))
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' })
})

// Time for mutation observer to notice change.
await waitFor(2000)
it('re-renders mermaid theme on html data-theme attribute change', () => {
const component = render(
<Mermaid chart={diagram} config={{}} />)

expect(mermaid.render).toBeCalledTimes(2)
expect(mermaid.initialize).toBeCalledTimes(2)
})
expectMermaidMatch(component)
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
expect(getThemeSpy).toBeCalledTimes(1)

it('renders the output of mermaid into the div', async () => {
const expectedOutput = 'mermaid output'
mermaid.render = jest.fn((_, __, cb) => {
if (cb) cb(expectedOutput, () => 0)
return expectedOutput
})
act(() => document.querySelector('html')!.setAttribute(HTML_THEME_ATTRIBUTE, DARK_THEME_KEY))

let component: any
renderer.act(() => {
component = renderer.create(
<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />
)
})
expectMermaidMatch(component)

expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)
expect(mermaid.render).toBeCalledTimes(1)
expect(component.toJSON()).toMatchSnapshot()
})
expect(getThemeSpy).toBeCalledTimes(1)

describe('changing the theme at runtime', () => {
let useRefSpy: jest.SpyInstance
let html: HTMLHtmlElement
act(() => document.querySelector('html')!.setAttribute(HTML_THEME_ATTRIBUTE, LIGHT_THEME_KEY))

beforeEach(() => {
html = document.createElement('html')
html.setAttribute(HTML_THEME_ATTRIBUTE, LIGHT_THEME_KEY)
useRefSpy = jest.spyOn(document, 'querySelector').mockReturnValue(html)
})
expectMermaidMatch(component)
})

afterEach(() => {
expect(useRefSpy).toHaveBeenCalled()
})
it('does not react to non-theme attribute changes of html', () => {
const component = render(<Mermaid chart={diagram} config={{}} />)

it('reacts to changed theme', async () => {
const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme')
renderer.act(() => {
renderer.create(
<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />
)
})

await renderer.act(async () => {
html.setAttribute(HTML_THEME_ATTRIBUTE, DARK_THEME_KEY)
await waitFor(1000)
})

expect(getThemeSpy.mock.calls.length).toBeGreaterThan(2)
})
expectMermaidMatch(component)
expect(mermaid.contentLoaded).toBeCalledTimes(1)
expect(mermaid.initialize).toBeCalledTimes(1)

it('does not react to non-theme attribute changes of html', async () => {
const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme')
renderer.act(() => {
renderer.create(
<Mermaid chart={`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`} config={{}} />
)
})

await renderer.act(async () => {
html.setAttribute('manifest', 'some-value')
await waitFor(1000)
})
expect(getThemeSpy).toHaveBeenCalledTimes(2)
})
act(() => document.querySelector('html')!.setAttribute('manifest', 'some-value'))

expectMermaidMatch(component)
})
Loading

0 comments on commit f2b9938

Please sign in to comment.