diff --git a/awx/ui_next/src/components/Lookup/Lookup.test.jsx b/awx/ui_next/src/components/Lookup/Lookup.test.jsx index 0729d537c2..186f231f11 100644 --- a/awx/ui_next/src/components/Lookup/Lookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/Lookup.test.jsx @@ -1,285 +1,362 @@ /* eslint-disable react/jsx-pascal-case */ import React from 'react'; import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import Lookup, { _Lookup } from './Lookup'; let mockData = [{ name: 'foo', id: 1, isChecked: false }]; const mockColumns = [{ name: 'Name', key: 'name', isSortable: true }]; -describe('', () => { - test('initially renders succesfully', () => { - mountWithContexts( - {}} - getItems={() => {}} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ); + +/** + * Check that an element is present on the document body + * @param {selector} query selector + */ +function checkRootElementPresent(selector) { + const queryResult = global.document.querySelector(selector); + expect(queryResult).not.toEqual(null); +} + +/** + * Check that an element isn't present on the document body + * @param {selector} query selector + */ +function checkRootElementNotPresent(selector) { + const queryResult = global.document.querySelector(selector); + expect(queryResult).toEqual(null); +} + +/** + * Check lookup input group tags for expected values + * @param {wrapper} enzyme wrapper instance + * @param {expected} array of expected tag values + */ +async function checkInputTagValues(wrapper, expected) { + checkRootElementNotPresent('body div[role="dialog"]'); + // check input group chip values + const chips = await waitForElement( + wrapper, + 'Lookup InputGroup Chip span', + el => el.length === expected.length + ); + expect(chips).toHaveLength(expected.length); + chips.forEach((el, index) => { + expect(el.text()).toEqual(expected[index]); }); +} - test('API response is formatted properly', done => { - const wrapper = mountWithContexts( - {}} - getItems={() => ({ - data: { results: [{ name: 'test instance', id: 1 }] }, - })} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ).find('Lookup'); - - setImmediate(() => { - expect(wrapper.state().results).toEqual([ - { id: 1, name: 'test instance' }, - ]); - done(); - }); +/** + * Check lookup modal list for expected values + * @param {wrapper} enzyme wrapper instance + * @param {expected} array of [selected, text] pairs describing + * the expected visible state of the modal data list + */ +async function checkModalListValues(wrapper, expected) { + // fail if modal isn't actually visible + checkRootElementPresent('body div[role="dialog"]'); + // check list item values + const rows = await waitForElement( + wrapper, + 'DataListItemRow', + el => el.length === expected.length + ); + expect(rows).toHaveLength(expected.length); + rows.forEach((el, index) => { + const [expectedChecked, expectedText] = expected[index]; + expect(expectedText).toEqual(el.text()); + expect(expectedChecked).toEqual(el.find('input').props().checked); }); +} - test('Opens modal when search icon is clicked', () => { - const spy = jest.spyOn(_Lookup.prototype, 'handleModalToggle'); - const mockSelected = [{ name: 'foo', id: 1 }]; - const wrapper = mountWithContexts( +/** + * Check lookup modal selection tags for expected values + * @param {wrapper} enzyme wrapper instance + * @param {expected} array of expected tag values + */ +async function checkModalTagValues(wrapper, expected) { + // fail if modal isn't actually visible + checkRootElementPresent('body div[role="dialog"]'); + // check modal chip values + const chips = await waitForElement( + wrapper, + 'Modal Chip span', + el => el.length === expected.length + ); + expect(chips).toHaveLength(expected.length); + chips.forEach((el, index) => { + expect(el.text()).toEqual(expected[index]); + }); +} + +describe('', () => { + let wrapper; + let onChange; + + beforeEach(() => { + const mockSelected = [{ name: 'foo', id: 1, url: '/api/v2/item/1' }]; + onChange = jest.fn(); + document.body.innerHTML = ''; + wrapper = mountWithContexts( {}} - getItems={() => {}} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ).find('Lookup'); - expect(spy).not.toHaveBeenCalled(); - expect(wrapper.state('lookupSelectedItems')).toEqual(mockSelected); - const searchItem = wrapper.find('button[aria-label="Search"]'); - searchItem.first().simulate('click'); - expect(spy).toHaveBeenCalled(); - expect(wrapper.state('lookupSelectedItems')).toEqual([ - { - id: 1, - name: 'foo', - }, - ]); - expect(wrapper.state('isModalOpen')).toEqual(true); - }); - - test('calls "toggleSelected" when a user changes a checkbox', done => { - const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected'); - const mockSelected = [{ name: 'foo', id: 1 }]; - const data = { - results: [{ name: 'test instance', id: 1, url: '/foo' }], - count: 1, - }; - const wrapper = mountWithContexts( - {}} - getItems={() => ({ data })} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ); - setImmediate(() => { - const searchItem = wrapper.find('button[aria-label="Search"]'); - searchItem.first().simulate('click'); - wrapper.find('input[type="checkbox"]').simulate('change'); - expect(spy).toHaveBeenCalled(); - done(); - }); - }); - - test('calls "toggleSelected" when remove icon is clicked', () => { - const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected'); - mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }]; - const data = { - results: [{ name: 'test instance', id: 1, url: '/foo' }], - count: 1, - }; - const wrapper = mountWithContexts( - {}} - getItems={() => ({ data })} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ); - const removeIcon = wrapper.find('button[aria-label="close"]').first(); - removeIcon.simulate('click'); - expect(spy).toHaveBeenCalled(); - }); - - test('renders chips from prop value', () => { - mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }]; - const wrapper = mountWithContexts( - {}} - value={mockData} - selected={[]} - getItems={() => {}} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ).find('Lookup'); - const chip = wrapper.find('.pf-c-chip'); - expect(chip).toHaveLength(2); - }); - - test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => { - mockData = []; - const wrapper = mountWithContexts( - {}} - value={mockData} - getItems={() => {}} - columns={mockColumns} - sortedColumnKey="name" - multiple - /> - ).find('Lookup'); - wrapper.instance().toggleSelected({ - id: 1, - name: 'foo', - }); - expect(wrapper.state('lookupSelectedItems')).toEqual([ - { - id: 1, - name: 'foo', - }, - ]); - wrapper.instance().toggleSelected({ - id: 1, - name: 'foo', - }); - expect(wrapper.state('lookupSelectedItems')).toEqual([]); - }); - - test('saveModal calls callback with selected items', () => { - mockData = []; - const onLookupSaveFn = jest.fn(); - const wrapper = mountWithContexts( - {}} - sortedColumnKey="name" - multiple - /> - ).find('Lookup'); - wrapper.instance().toggleSelected({ - id: 1, - name: 'foo', - }); - expect(wrapper.state('lookupSelectedItems')).toEqual([ - { - id: 1, - name: 'foo', - }, - ]); - wrapper.instance().saveModal(); - expect(onLookupSaveFn).toHaveBeenCalledWith( - [ - { - id: 1, - name: 'foo', - }, - ], - 'fooBar' - ); - }); - - test('should call callback with selected single item', () => { - mockData = { name: 'foo', id: 1, isChecked: false, url: 'https://foo' }; - const onLookupSaveFn = jest.fn(); - const wrapper = mountWithContexts( - ({ data: { - results: [mockData], - count: 1, + count: 2, + results: [ + ...mockSelected, + { name: 'bar', id: 2, url: '/api/v2/item/2' }, + ], }, })} + columns={mockColumns} sortedColumnKey="name" /> ); + }); + + test('Initially renders succesfully', () => { + expect(wrapper.find('Lookup')).toHaveLength(1); + }); + + test('Expected items are shown', async done => { + expect(wrapper.find('Lookup')).toHaveLength(1); + await checkInputTagValues(wrapper, ['foo']); + done(); + }); + + test('Open and close modal', async done => { + checkRootElementNotPresent('body div[role="dialog"]'); + wrapper.find('button[aria-label="Search"]').simulate('click'); + checkRootElementPresent('body div[role="dialog"]'); + // This check couldn't pass unless api response was formatted properly + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + wrapper.find('Modal button[aria-label="Close"]').simulate('click'); + checkRootElementNotPresent('body div[role="dialog"]'); + wrapper.find('button[aria-label="Search"]').simulate('click'); + checkRootElementPresent('body div[role="dialog"]'); wrapper - .find('Lookup') - .instance() - .toggleSelected({ - id: 1, - name: 'foo', - }); + .find('Modal button') + .findWhere(e => e.text() === 'Cancel') + .first() + .simulate('click'); + checkRootElementNotPresent('body div[role="dialog"]'); + done(); + }); + + test('Add item with checkbox then save', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); wrapper - .find('Lookup') - .instance() - .saveModal(); - expect(onLookupSaveFn).toHaveBeenCalledWith( - { - id: 1, - name: 'foo', - }, - 'fooBar' + .find('DataListItemRow') + .findWhere(el => el.text() === 'bar') + .find('input[type="checkbox"]') + .simulate('change'); + await checkModalListValues(wrapper, [[true, 'foo'], [true, 'bar']]); + wrapper + .find('Modal button') + .findWhere(e => e.text() === 'Save') + .first() + .simulate('click'); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].map(({ name }) => name)).toEqual([ + 'foo', + 'bar', + ]); + done(); + }); + + test('Add item with checkbox then cancel', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + wrapper + .find('DataListItemRow') + .findWhere(el => el.text() === 'bar') + .find('input[type="checkbox"]') + .simulate('change'); + await checkModalListValues(wrapper, [[true, 'foo'], [true, 'bar']]); + wrapper + .find('Modal button') + .findWhere(e => e.text() === 'Cancel') + .first() + .simulate('click'); + expect(onChange).toHaveBeenCalledTimes(0); + await checkInputTagValues(wrapper, ['foo']); + done(); + }); + + test('Remove item with checkbox', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, ['foo']); + wrapper + .find('DataListItemRow') + .findWhere(el => el.text() === 'foo') + .find('input[type="checkbox"]') + .simulate('change'); + await checkModalListValues(wrapper, [[false, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, []); + done(); + }); + + test('Remove item with selected icon button', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, ['foo']); + wrapper + .find('Modal Chip') + .findWhere(el => el.text() === 'foo') + .first() + .find('button') + .simulate('click'); + await checkModalListValues(wrapper, [[false, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, []); + done(); + }); + + test('Remove item with input group button', async done => { + await checkInputTagValues(wrapper, ['foo']); + wrapper + .find('Lookup InputGroup Chip') + .findWhere(el => el.text() === 'foo') + .first() + .find('button') + .simulate('click'); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith([], 'foobar'); + done(); + }); +}); + +describe('', () => { + let wrapper; + let onChange; + + beforeEach(() => { + const mockSelected = { name: 'foo', id: 1, url: '/api/v2/item/1' }; + onChange = jest.fn(); + document.body.innerHTML = ''; + wrapper = mountWithContexts( + ({ + data: { + count: 2, + results: [ + mockSelected, + { name: 'bar', id: 2, url: '/api/v2/item/2' }, + ], + }, + })} + columns={mockColumns} + sortedColumnKey="name" + /> ); }); - test('should re-fetch data when URL params change', async () => { + test('Initially renders succesfully', () => { + expect(wrapper.find('Lookup')).toHaveLength(1); + }); + + test('Expected items are shown', async done => { + expect(wrapper.find('Lookup')).toHaveLength(1); + await checkInputTagValues(wrapper, ['foo']); + done(); + }); + + test('Open and close modal', async done => { + checkRootElementNotPresent('body div[role="dialog"]'); + wrapper.find('button[aria-label="Search"]').simulate('click'); + checkRootElementPresent('body div[role="dialog"]'); + // This check couldn't pass unless api response was formatted properly + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + wrapper.find('Modal button[aria-label="Close"]').simulate('click'); + checkRootElementNotPresent('body div[role="dialog"]'); + wrapper.find('button[aria-label="Search"]').simulate('click'); + checkRootElementPresent('body div[role="dialog"]'); + wrapper + .find('Modal button') + .findWhere(e => e.text() === 'Cancel') + .first() + .simulate('click'); + checkRootElementNotPresent('body div[role="dialog"]'); + done(); + }); + + test('Change selected item with radio control then save', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, ['foo']); + wrapper + .find('DataListItemRow') + .findWhere(el => el.text() === 'bar') + .find('input[type="radio"]') + .simulate('change'); + await checkModalListValues(wrapper, [[false, 'foo'], [true, 'bar']]); + await checkModalTagValues(wrapper, ['bar']); + wrapper + .find('Modal button') + .findWhere(e => e.text() === 'Save') + .first() + .simulate('click'); + expect(onChange).toHaveBeenCalledTimes(1); + const [[{ name }]] = onChange.mock.calls; + expect(name).toEqual('bar'); + done(); + }); + + test('Change selected item with checkbox then cancel', async done => { + wrapper.find('button[aria-label="Search"]').simulate('click'); + await checkModalListValues(wrapper, [[true, 'foo'], [false, 'bar']]); + await checkModalTagValues(wrapper, ['foo']); + wrapper + .find('DataListItemRow') + .findWhere(el => el.text() === 'bar') + .find('input[type="radio"]') + .simulate('change'); + await checkModalListValues(wrapper, [[false, 'foo'], [true, 'bar']]); + await checkModalTagValues(wrapper, ['bar']); + wrapper + .find('Modal button') + .findWhere(e => e.text() === 'Cancel') + .first() + .simulate('click'); + expect(onChange).toHaveBeenCalledTimes(0); + done(); + }); + + test('should re-fetch data when URL params change', async done => { mockData = [{ name: 'foo', id: 1, isChecked: false }]; const history = createMemoryHistory({ initialEntries: ['/organizations/add'], }); const getItems = jest.fn(); - const wrapper = mountWithContexts( + const LookupWrapper = mountWithContexts( <_Lookup + multiple + name="foo" lookupHeader="Foo Bar" onLookupSave={() => {}} value={mockData} - selected={[]} columns={mockColumns} sortedColumnKey="name" getItems={getItems} - multiple - handleHttpError={() => {}} location={{ history }} i18n={{ _: val => val.toString() }} /> ); - expect(getItems).toHaveBeenCalledTimes(1); history.push('organizations/add?page=2'); - wrapper.setProps({ + LookupWrapper.setProps({ location: { history }, }); - wrapper.update(); + LookupWrapper.update(); expect(getItems).toHaveBeenCalledTimes(2); + done(); }); });