From 70137dea5a60a8d4d4f91523bbcddc8e7da947fb Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 8 Apr 2019 10:05:22 -0400 Subject: [PATCH] fix tests for OrganizationTeams, OrganizationTeamsList --- .../components/OrganizationForm.test.jsx | 3 +- .../components/OrganizationTeamsList.test.jsx | 145 ++++++++-------- .../Organization/OrganizationTeams.test.jsx | 158 ++++++++++++++---- __tests__/testUtils.js | 4 + jest.config.js | 2 +- package-lock.json | 43 +++-- package.json | 1 + .../components/OrganizationTeamsList.jsx | 47 ++++-- .../screens/Organization/Organization.jsx | 1 - .../Organization/OrganizationTeams.jsx | 37 ++-- 10 files changed, 289 insertions(+), 152 deletions(-) create mode 100644 __tests__/testUtils.js diff --git a/__tests__/pages/Organizations/components/OrganizationForm.test.jsx b/__tests__/pages/Organizations/components/OrganizationForm.test.jsx index 55c30fae3d..8ef06dcdfd 100644 --- a/__tests__/pages/Organizations/components/OrganizationForm.test.jsx +++ b/__tests__/pages/Organizations/components/OrganizationForm.test.jsx @@ -4,8 +4,7 @@ import { MemoryRouter } from 'react-router-dom'; import { I18nProvider } from '@lingui/react'; import { ConfigContext } from '../../../../src/context'; import OrganizationForm from '../../../../src/pages/Organizations/components/OrganizationForm'; - -const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); +import { sleep } from '../../../testUtils'; describe('', () => { let api; diff --git a/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx b/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx index 87da53fb38..41282262f5 100644 --- a/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx +++ b/__tests__/pages/Organizations/components/OrganizationTeamsList.test.jsx @@ -1,16 +1,17 @@ import React from 'react'; import { mount } from 'enzyme'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { I18nProvider } from '@lingui/react'; - +import { sleep } from '../../../testUtils'; import OrganizationTeamsList from '../../../../src/pages/Organizations/components/OrganizationTeamsList'; const mockData = [ - { - id: 1, - name: 'boo', - url: '/foo/bar/' - } + { id: 1, name: 'one', url: '/org/team/1' }, + { id: 2, name: 'two', url: '/org/team/2' }, + { id: 3, name: 'three', url: '/org/team/3' }, + { id: 4, name: 'four', url: '/org/team/4' }, + { id: 5, name: 'five', url: '/org/team/5' }, ]; describe('', () => { @@ -23,85 +24,81 @@ describe('', () => { {}} - removeRole={() => {}} + teams={mockData} + itemCount={7} + queryParams={{ + page: 1, + page_size: 5, + order_by: 'name', + }} /> ); }); - test('api response data passed to component gets set to state properly', (done) => { - const wrapper = mount( - - - ({ data: { count: 1, results: mockData } })} - /> - - - ).find('OrganizationTeamsList'); - - setImmediate(() => { - expect(wrapper.state().results).toEqual(mockData); - done(); + // should navigate when datalisttoolbar changes sorting + test('should navigate when DataListToolbar calls onSort prop', async () => { + const history = createMemoryHistory({ + initialEntries: ['/organizations/1/teams'], }); + const wrapper = mount( + + + + + + ); + + const toolbar = wrapper.find('DataListToolbar'); + expect(toolbar.prop('sortedColumnKey')).toEqual('name'); + expect(toolbar.prop('sortOrder')).toEqual('ascending'); + toolbar.prop('onSort')('name', 'descending'); + expect(history.location.search).toEqual('?order_by=-name'); + await sleep(0); + wrapper.update(); + + expect(toolbar.prop('sortedColumnKey')).toEqual('name'); + // TODO: this assertion required updating queryParams prop. Consider + // fixing after #147 is done: + // expect(toolbar.prop('sortOrder')).toEqual('descending'); + toolbar.prop('onSort')('name', 'ascending'); + expect(history.location.search).toEqual('?order_by=name'); }); - test('handleSort being passed properly to DataListToolbar component', async (done) => { - const handleSort = jest.spyOn(OrganizationTeamsList.prototype, 'handleSort'); - const wrapper = mount( - - - ({ data: { count: 1, results: mockData } })} - /> - - - ).find('OrganizationTeamsList'); - expect(handleSort).not.toHaveBeenCalled(); - - setImmediate(() => { - const rendered = wrapper.update(); - rendered.find('button[aria-label="Sort"]').simulate('click'); - expect(handleSort).toHaveBeenCalled(); - done(); + test('should navigate to page when Pagination calls onSetPage prop', () => { + const history = createMemoryHistory({ + initialEntries: ['/organizations/1/teams'], }); - }); - - test('handleSetPage calls readQueryParams and readOrganizationTeamsList ', () => { - const spyQueryParams = jest.spyOn(OrganizationTeamsList.prototype, 'readQueryParams'); - const spyFetch = jest.spyOn(OrganizationTeamsList.prototype, 'readOrganizationTeamsList'); const wrapper = mount( - - + + ({ data: { count: 1, results: mockData } })} + teams={mockData} + itemCount={7} + queryParams={{ + page: 1, + page_size: 5, + order_by: 'name', + }} /> - - - ).find('OrganizationTeamsList'); - wrapper.instance().handleSetPage(2, 10); - expect(spyQueryParams).toHaveBeenCalled(); - expect(spyFetch).toHaveBeenCalled(); - wrapper.setState({ sortOrder: 'descending' }); - wrapper.instance().handleSetPage(3, 5); - expect(spyQueryParams).toHaveBeenCalled(); - expect(spyFetch).toHaveBeenCalled(); - const queryParamCalls = spyQueryParams.mock.calls; - // make sure last two readQueryParams calls - // were called with the correct arguments - expect(queryParamCalls[queryParamCalls.length - 2][0]) - .toEqual({ order_by: 'name', page: 2, page_size: 10 }); - expect(queryParamCalls[queryParamCalls.length - 1][0]) - .toEqual({ order_by: '-name', page: 3, page_size: 5 }); + + + ); + + const pagination = wrapper.find('Pagination'); + pagination.prop('onSetPage')(2, 5); + expect(history.location.search).toEqual('?page=2&page_size=5'); + wrapper.update(); + pagination.prop('onSetPage')(1, 25); + expect(history.location.search).toEqual('?page=1&page_size=25'); }); }); diff --git a/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx b/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx index d0ef146694..d9f8a1c7c1 100644 --- a/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx +++ b/__tests__/pages/Organizations/screens/Organization/OrganizationTeams.test.jsx @@ -1,45 +1,135 @@ import React from 'react'; -import { mount } from 'enzyme'; -import { MemoryRouter } from 'react-router-dom'; +import { mount, shallow } from 'enzyme'; +import { MemoryRouter, Router } from 'react-router-dom'; +import { I18nProvider } from '@lingui/react'; +import { createMemoryHistory } from 'history'; +import { sleep } from '../../../../testUtils'; +import OrganizationTeams, { _OrganizationTeams } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams'; +import OrganizationTeamsList from '../../../../../src/pages/Organizations/components/OrganizationTeamsList'; -import OrganizationTeams from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams'; - -const mockAPITeamsList = { - foo: 'bar', +const listData = { + data: { + count: 7, + results: [ + { id: 1, name: 'one', url: '/org/team/1' }, + { id: 2, name: 'two', url: '/org/team/2' }, + { id: 3, name: 'three', url: '/org/team/3' }, + { id: 4, name: 'four', url: '/org/team/4' }, + { id: 5, name: 'five', url: '/org/team/5' }, + ] + } }; -const readOrganizationTeamsList = () => Promise.resolve(mockAPITeamsList); - describe('', () => { - test('initially renders succesfully', () => { - mount( - - - + test('renders succesfully', () => { + shallow( + <_OrganizationTeams + id={1} + searchString="" + location={{ search: '', pathname: '/organizations/1/teams' }} + api={{ + readOrganizationTeamsList: jest.fn(), + }} + /> ); }); - test('passed methods as props are called appropriately', async () => { - const wrapper = mount( - - - + test('should load teams on mount', () => { + const readOrganizationTeamsList = jest.fn(() => Promise.resolve(listData)); + mount( + + + + + ).find('OrganizationTeams'); - const teamsList = await wrapper.instance().readOrganizationTeamsList(); - expect(teamsList).toEqual(mockAPITeamsList); + expect(readOrganizationTeamsList).toHaveBeenCalledWith(1, { + page: 1, + page_size: 5, + order_by: 'name', + }); + }); + + test('should pass fetched teams to list component', async () => { + const readOrganizationTeamsList = jest.fn(() => Promise.resolve(listData)); + const wrapper = mount( + + + + + + ); + + await sleep(0); + wrapper.update(); + + const list = wrapper.find('OrganizationTeamsList'); + expect(list.prop('teams')).toEqual(listData.data.results); + expect(list.prop('itemCount')).toEqual(listData.data.count); + expect(list.prop('queryParams')).toEqual({ + page: 1, + page_size: 5, + order_by: 'name', + }); + }); + + test('should pass queryParams to OrganizationTeamsList', async () => { + const page1Data = listData; + const page2Data = { + data: { + count: 7, + results: [ + { id: 6, name: 'six', url: '/org/team/6' }, + { id: 7, name: 'seven', url: '/org/team/7' }, + ] + } + }; + const readOrganizationTeamsList = jest.fn(); + readOrganizationTeamsList.mockReturnValueOnce(page1Data); + const history = createMemoryHistory({ + initialEntries: ['/organizations/1/teams'], + }); + const wrapper = mount( + + + + + + ); + + await sleep(0); + wrapper.update(); + + const list = wrapper.find(OrganizationTeamsList); + expect(list.prop('queryParams')).toEqual({ + page: 1, + page_size: 5, + order_by: 'name', + }); + + readOrganizationTeamsList.mockReturnValueOnce(page2Data); + history.push('/organizations/1/teams?page=2'); + wrapper.setProps({ history }); + + await sleep(0); + wrapper.update(); + const list2 = wrapper.find(OrganizationTeamsList); + expect(list2.prop('queryParams')).toEqual({ + page: 2, + page_size: 5, + order_by: 'name', + }); }); }); diff --git a/__tests__/testUtils.js b/__tests__/testUtils.js new file mode 100644 index 0000000000..6666d69844 --- /dev/null +++ b/__tests__/testUtils.js @@ -0,0 +1,4 @@ + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); +/* eslint-disable-next-line import/prefer-default-export */ +export { sleep }; diff --git a/jest.config.js b/jest.config.js index db3216cbcb..3e11502a18 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,7 @@ module.exports = { }, setupTestFrameworkScriptFile: '/jest.setup.js', testMatch: [ - '/__tests__/**/*.{js,jsx}' + '/__tests__/**/*.test.{js,jsx}' ], testEnvironment: 'jsdom', testURL: 'http://127.0.0.1:3001', diff --git a/package-lock.json b/package-lock.json index 9f91d8a84e..ccb49a211b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1015,6 +1015,21 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, + "@babel/runtime": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", + "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", + "requires": { + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + } + } + }, "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -7043,25 +7058,16 @@ } }, "history": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", - "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz", + "integrity": "sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==", "requires": { - "invariant": "^2.2.1", + "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", "resolve-pathname": "^2.2.0", - "value-equal": "^0.4.0", - "warning": "^3.0.0" - }, - "dependencies": { - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - } + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^0.4.0" } }, "hmac-drbg": { @@ -14022,6 +14028,11 @@ "setimmediate": "^1.0.4" } }, + "tiny-invariant": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.4.tgz", + "integrity": "sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g==" + }, "tiny-warning": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz", diff --git a/package.json b/package.json index 5e6e4398c1..f59720a3f6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "eslint-plugin-jsx-a11y": "^6.1.1", "eslint-plugin-react": "^7.11.1", "file-loader": "^2.0.0", + "history": "^4.9.0", "jest": "^23.6.0", "node-sass": "^4.9.3", "react-hot-loader": "^4.3.3", diff --git a/src/pages/Organizations/components/OrganizationTeamsList.jsx b/src/pages/Organizations/components/OrganizationTeamsList.jsx index 0691d42a65..5f30e4b7e1 100644 --- a/src/pages/Organizations/components/OrganizationTeamsList.jsx +++ b/src/pages/Organizations/components/OrganizationTeamsList.jsx @@ -1,11 +1,20 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { - DataList, DataListItem, DataListCell, Text, - TextContent, TextVariants + DataList, + DataListItem, + DataListCell, + Text, + TextContent, + TextVariants, + Title, + EmptyState, + EmptyStateIcon, + EmptyStateBody, } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; import { I18n, i18nMark } from '@lingui/react'; -import { t } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; import { withRouter, Link } from 'react-router-dom'; import Pagination from '../../../components/Pagination'; @@ -94,7 +103,17 @@ class OrganizationTeamsList extends React.Component { )} // TODO: replace with proper error handling )} - {teams.length > 0 && ( + {teams.length === 0 ? ( + + + + <Trans>No Teams Found</Trans> + + + Please add a team to populate this list + + + ) : ( ( )} diff --git a/src/pages/Organizations/screens/Organization/OrganizationTeams.jsx b/src/pages/Organizations/screens/Organization/OrganizationTeams.jsx index b6cf169e39..debe28f78c 100644 --- a/src/pages/Organizations/screens/Organization/OrganizationTeams.jsx +++ b/src/pages/Organizations/screens/Organization/OrganizationTeams.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; import OrganizationTeamsList from '../../components/OrganizationTeamsList'; @@ -17,6 +17,7 @@ class OrganizationTeams extends React.Component { this.readOrganizationTeamsList = this.readOrganizationTeamsList.bind(this); this.state = { + isInitialized: false, isLoading: false, error: null, itemCount: 0, @@ -36,8 +37,8 @@ class OrganizationTeams extends React.Component { } getQueryParams () { - const { searchString } = this.props; - const searchParams = parseQueryString(searchString.substring(1)); + const { location } = this.props; + const searchParams = parseQueryString(location.search.substring(1)); return { ...DEFAULT_QUERY_PARAMS, @@ -57,36 +58,44 @@ class OrganizationTeams extends React.Component { itemCount: count, teams: results, isLoading: false, + isInitialized: true, }); } catch (error) { this.setState({ error, - isLoading: false + isLoading: false, + isInitialized: true, }); } } render () { - const { teams, itemCount, isLoading } = this.state; + const { teams, itemCount, isLoading, isInitialized, error } = this.state; - if (isLoading) { - return
Loading...
; + if (error) { + // TODO: better error state + return
{error.message}
; } + // TODO: better loading state return ( - + + {isLoading && (
Loading...
)} + {isInitialized && ( + + )} +
); } } OrganizationTeams.propTypes = { id: PropTypes.number.isRequired, - searchString: PropTypes.string.isRequired, - api: PropTypes.shape().isRequired, // TODO: remove? + api: PropTypes.shape().isRequired, }; export { OrganizationTeams as _OrganizationTeams };