From 436adf90ad29333835889a6e8693d7970bdc7ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Pe=C3=B1a?= Date: Mon, 1 Feb 2021 12:53:50 -0800 Subject: [PATCH] search: add tests for Layout.tsx search URL parsing (#17785) Needed to add [jest-canvas-mock](https://www.npmjs.com/package/jest-canvas-mock) library to mock canvas on Jest, our unit test runner, as Monaco requires canvas. I tried doing a shallow test to avoid this, but unfortunately [Enzyme's shallow rendering does not call `React.useEffect`](https://github.com/enzymejs/enzyme/issues/2086), which is necessary to test this functionality. --- client/shared/src/util/searchTestHelpers.ts | 4 + client/web/jest.config.js | 1 + client/web/src/Layout.test.tsx | 376 ++++++++++++++++++++ client/web/src/Layout.tsx | 2 +- package.json | 1 + yarn.lock | 28 +- 6 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 client/web/src/Layout.test.tsx diff --git a/client/shared/src/util/searchTestHelpers.ts b/client/shared/src/util/searchTestHelpers.ts index fb19e5ee04cd0..3c328e35d03a9 100644 --- a/client/shared/src/util/searchTestHelpers.ts +++ b/client/shared/src/util/searchTestHelpers.ts @@ -262,6 +262,10 @@ export const NOOP_SETTINGS_CASCADE = { const services = { contribution: { getContributions: () => of({}), + registerContributions: () => {}, + }, + commands: { + registerCommand: () => {}, }, } diff --git a/client/web/jest.config.js b/client/web/jest.config.js index 5ee4a3417abd1..54f9c2fca66a5 100644 --- a/client/web/jest.config.js +++ b/client/web/jest.config.js @@ -7,6 +7,7 @@ const exportedConfig = { ...config, displayName: 'web', rootDir: __dirname, + setupFiles: [...config.setupFiles, 'jest-canvas-mock'], // mocking canvas is required for Monaco editor to work in unit tests } module.exports = exportedConfig diff --git a/client/web/src/Layout.test.tsx b/client/web/src/Layout.test.tsx new file mode 100644 index 0000000000000..f653529923a2b --- /dev/null +++ b/client/web/src/Layout.test.tsx @@ -0,0 +1,376 @@ +import { mount } from 'enzyme' +import { createBrowserHistory } from 'history' +import React from 'react' +import { BrowserRouter } from 'react-router-dom' +import { NEVER } from 'rxjs' +import sinon from 'sinon' +import { extensionsController } from '../../shared/src/util/searchTestHelpers' +import { SearchPatternType } from './graphql-operations' +import { Layout, LayoutProps } from './Layout' + +describe('Layout', () => { + const defaultProps: LayoutProps = ({ + // Parsed query components + parsedSearchQuery: 'r:golang/oauth2 test f:travis', + setParsedSearchQuery: () => {}, + patternType: SearchPatternType.literal, + setPatternType: () => {}, + caseSensitive: false, + setCaseSensitivity: () => {}, + versionContext: undefined, + setVersionContext: () => {}, + + // Other minimum props required to render + routes: [], + navbarSearchQueryState: { query: '' }, + onNavbarQueryChange: () => {}, + settingsCascade: { + subjects: null, + final: null, + }, + keyboardShortcuts: [], + extensionsController, + platformContext: { forceUpdateTooltip: () => {}, settings: NEVER }, + } as unknown) as LayoutProps + + beforeEach(() => { + const root = document.createElement('div') + root.id = 'root' + document.body.append(root) + }) + + afterEach(() => { + document.querySelector('#root')?.remove() + }) + + it('should update parsedSearchQuery if different between URL and context', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis2&patternType=regexp' }) + + const setParsedSearchQuery = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setParsedSearchQuery) + sinon.assert.calledWith(setParsedSearchQuery, 'r:golang/oauth2 test f:travis2') + + element.unmount() + }) + + it('should not update parsedSearchQuery if URL and context are the same', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis&patternType=regexp' }) + + const setParsedSearchQuery = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setParsedSearchQuery) + + element.unmount() + }) + + it('should update parsedSearchQuery if changing to empty', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=&patternType=regexp' }) + + const setParsedSearchQuery = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setParsedSearchQuery) + sinon.assert.calledWith(setParsedSearchQuery, '') + + element.unmount() + }) + + it('should update patternType if different between URL and context', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis&patternType=regexp' }) + + const setPatternTypeSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setPatternTypeSpy) + sinon.assert.calledWith(setPatternTypeSpy, SearchPatternType.regexp) + + element.unmount() + }) + + it('should not update patternType if URL and context are the same', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis&patternType=regexp' }) + + const setPatternTypeSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setPatternTypeSpy) + + element.unmount() + }) + + it('should not update patternType if query is empty', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=&patternType=regexp' }) + + const setPatternTypeSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setPatternTypeSpy) + + element.unmount() + }) + + it('should update caseSensitive if different between URL and context', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis case:yes' }) + + const setCaseSensitivitySpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setCaseSensitivitySpy) + sinon.assert.calledWith(setCaseSensitivitySpy, true) + + element.unmount() + }) + + it('should not update caseSensitive if URL and context are the same', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis+case:yes' }) + + const setCaseSensitivitySpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setCaseSensitivitySpy) + + element.unmount() + }) + + it('should not update caseSensitive if query is empty', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=case:yes' }) + + const setCaseSensitivitySpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setCaseSensitivitySpy) + + element.unmount() + }) + + it('should update versionContext if different between URL and context', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis&c=test' }) + + const setVersionContextSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setVersionContextSpy) + sinon.assert.calledWith(setVersionContextSpy, 'test') + + element.unmount() + }) + + it('should not update versionContext if same between URL and context', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis&c=test' }) + + const setVersionContextSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setVersionContextSpy) + + element.unmount() + }) + + it('should clear versionContext if updating to clear', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=r:golang/oauth2+test+f:travis' }) + + const setVersionContextSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.calledOnce(setVersionContextSpy) + sinon.assert.calledWith(setVersionContextSpy, undefined) + + element.unmount() + }) + + it('should not update versionContext if query is empty', () => { + const history = createBrowserHistory() + history.replace({ search: 'q=&c=test' }) + + const setVersionContextSpy = sinon.spy() + + const element = mount( + + + , + { attachTo: document.querySelector('#root') as HTMLElement } + ) + + sinon.assert.notCalled(setVersionContextSpy) + + element.unmount() + }) +}) diff --git a/client/web/src/Layout.tsx b/client/web/src/Layout.tsx index b8e194a2a1b13..29e3d9c91e067 100644 --- a/client/web/src/Layout.tsx +++ b/client/web/src/Layout.tsx @@ -206,7 +206,7 @@ export const Layout: React.FunctionComponent = props => { // TODO add a component layer as the parent of the Layout component rendering "top-level" routes that do not render the navbar, // so that Layout can always render the navbar. - const needsSiteInit = window.context.needsSiteInit + const needsSiteInit = window.context?.needsSiteInit const isSiteInit = props.location.pathname === '/site-admin/init' const isSignInOrUp = props.location.pathname === '/sign-in' || diff --git a/package.json b/package.json index 2c9b064e5a374..bc5c9e9d01b26 100644 --- a/package.json +++ b/package.json @@ -200,6 +200,7 @@ "gulp": "^4.0.2", "identity-obj-proxy": "^3.0.0", "jest": "^25.5.4", + "jest-canvas-mock": "^2.3.0", "jsdom": "^15.2.1", "json-schema-ref-parser": "^9.0.6", "json-schema-to-typescript": "^9.1.1", diff --git a/yarn.lock b/yarn.lock index cf3cc73438fc5..6fc86ff3a015d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,7 +2702,8 @@ integrity sha512-KWxkyphmlwam8kfYPSmoitKQRMGQCsr1ZRmNZgijT7ABKaVyk/+I5ezt2J213tM04Hi0vyg4L7iH1VCkNvm2Jw== "@sourcegraph/extension-api-types@link:client/packages/@sourcegraph/extension-api-types": - version "2.1.0" + version "0.0.0" + uid "" "@sourcegraph/prettierrc@^3.0.3": version "3.0.3" @@ -7184,7 +7185,7 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -7835,6 +7836,11 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + cssnano-preset-default@^4.0.7: version "4.0.7" resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" @@ -13097,6 +13103,14 @@ jed@1.1.1: resolved "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" integrity sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ= +jest-canvas-mock@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.3.0.tgz#50f4cc178ae52c4c0e2ce4fd3a3ad2a41ad4eb36" + integrity sha512-3TMyR66VG2MzAW8Negzec03bbcIjVJMfGNvKzrEnbws1CYKqMNkvIJ8LbkoGYfp42tKqDmhIpQq3v+MNLW2A2w== + dependencies: + cssfontparser "^1.2.1" + moo-color "^1.0.2" + jest-changed-files@^25.5.0: version "25.5.0" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz#141cc23567ceb3f534526f8614ba39421383634c" @@ -15209,6 +15223,13 @@ monaco-editor@^0.18.1: resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc" integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw== +moo-color@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64" + integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg== + dependencies: + color-name "^1.1.4" + moo@^0.5.0: version "0.5.1" resolved "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" @@ -19749,7 +19770,8 @@ sourcegraph@^24.0.0: integrity sha512-uB2SbjEzcA/HWP3Rj+INUXqQweO8HmRdVWW7zesFWZQFBcu7JTcq0pg0jeAN71glKPIyyOgZVrIm1d7KoVzfZw== "sourcegraph@link:client/packages/sourcegraph-extension-api": - version "24.8.0" + version "0.0.0" + uid "" space-separated-tokens@^1.0.0: version "1.1.2"