diff --git a/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx b/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx index f3fbf92571cc..c650a4cdafe2 100644 --- a/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx @@ -17,77 +17,121 @@ * under the License. */ import React from 'react'; -import Button from 'src/components/Button'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; -import Select from 'src/components/Select'; import QuerySearch from 'src/SqlLab/components/QuerySearch'; +import { Provider } from 'react-redux'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import { fireEvent, render, screen, act } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import userEvent from '@testing-library/user-event'; + +const mockStore = configureStore([thunk]); +const store = mockStore({}); const SEARCH_ENDPOINT = 'glob:*/superset/search_queries?*'; +const USER_ENDPOINT = 'glob:*/api/v1/query/related/user'; +const DATABASE_ENDPOINT = 'glob:*/api/v1/database/?*'; fetchMock.get(SEARCH_ENDPOINT, []); +fetchMock.get(USER_ENDPOINT, []); +fetchMock.get(DATABASE_ENDPOINT, []); describe('QuerySearch', () => { - const search = sinon.spy(QuerySearch.prototype, 'refreshQueries'); const mockedProps = { actions: { addDangerToast: jest.fn() }, - height: 0, displayLimit: 50, }; + it('is valid', () => { - expect(React.isValidElement()).toBe(true); + expect( + React.isValidElement( + + + + + , + ), + ).toBe(true); }); - let wrapper; - beforeEach(() => { - wrapper = shallow(); + + beforeEach(async () => { + // You need this await function in order to change state in the app. In fact you need it everytime you re-render. + await act(async () => { + render( + + + + + , + ); + }); }); - it('should have three Select', () => { - expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(3); + it('should have three Selects', () => { + expect(screen.getByText(/28 days ago/i)).toBeInTheDocument(); + expect(screen.getByText(/now/i)).toBeInTheDocument(); + expect(screen.getByText(/success/i)).toBeInTheDocument(); }); it('updates fromTime on user selects from time', () => { - wrapper.find('[name="select-from"]').simulate('change', { value: 0 }); - expect(wrapper.state().from).toBe(0); + const role = screen.getByText(/28 days ago/i); + fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); + userEvent.click(screen.getByText(/1 hour ago/i)); + expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument(); }); - it('updates toTime on user selects to time', () => { - wrapper.find('[name="select-to"]').simulate('change', { value: 0 }); - expect(wrapper.state().to).toBe(0); + it('updates toTime on user selects on time', () => { + const role = screen.getByText(/now/i); + fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); + userEvent.click(screen.getByText(/1 hour ago/i)); + expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument(); }); it('updates status on user selects status', () => { - wrapper - .find('[name="select-status"]') - .simulate('change', { value: 'success' }); - expect(wrapper.state().status).toBe('success'); + const role = screen.getByText(/success/i); + fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); + userEvent.click(screen.getByText(/failed/i)); + expect(screen.getByText(/failed/i)).toBeInTheDocument(); }); it('should have one input for searchText', () => { - expect(wrapper.find('input')).toExist(); + expect( + screen.getByPlaceholderText(/Query search string/i), + ).toBeInTheDocument(); }); it('updates search text on user inputs search text', () => { - wrapper.find('input').simulate('change', { target: { value: 'text' } }); - expect(wrapper.state().searchText).toBe('text'); + const search = screen.getByPlaceholderText(/Query search string/i); + userEvent.type(search, 'text'); + expect(search.value).toBe('text'); }); - it('refreshes queries when enter (only) is pressed on the input', () => { - const { callCount } = search; - wrapper.find('input').simulate('keyDown', { keyCode: 'a'.charCodeAt(0) }); - expect(search.callCount).toBe(callCount); - wrapper.find('input').simulate('keyDown', { keyCode: '\r'.charCodeAt(0) }); - expect(search.callCount).toBe(callCount + 1); + it('should have one Button', () => { + const button = screen.getAllByRole('button'); + expect(button.length).toEqual(1); }); - it('should have one Button', () => { - expect(wrapper.find(Button)).toExist(); + it('should call API when search button is pressed', async () => { + fetchMock.resetHistory(); + const button = screen.getByRole('button'); + await act(async () => { + userEvent.click(button); + }); + expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1); }); - it('refreshes queries when clicked', () => { - const { callCount } = search; - wrapper.find(Button).simulate('click'); - expect(search.callCount).toBe(callCount + 1); + it('should call API when (only)enter key is pressed', async () => { + fetchMock.resetHistory(); + const search = screen.getByPlaceholderText(/Query search string/i); + await act(async () => { + userEvent.type(search, 'a'); + }); + expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(0); + await act(async () => { + userEvent.type(search, '{enter}'); + }); + expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1); }); }); diff --git a/superset-frontend/src/SqlLab/components/QuerySearch.jsx b/superset-frontend/src/SqlLab/components/QuerySearch.jsx deleted file mode 100644 index f5e188780ae6..000000000000 --- a/superset-frontend/src/SqlLab/components/QuerySearch.jsx +++ /dev/null @@ -1,330 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import Button from 'src/components/Button'; -import Select from 'src/components/Select'; -import { styled, t, SupersetClient } from '@superset-ui/core'; - -import Loading from '../../components/Loading'; -import QueryTable from './QueryTable'; -import { - now, - epochTimeXHoursAgo, - epochTimeXDaysAgo, - epochTimeXYearsAgo, -} from '../../modules/dates'; -import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants'; -import AsyncSelect from '../../components/AsyncSelect'; - -const propTypes = { - actions: PropTypes.object.isRequired, - height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - displayLimit: PropTypes.number.isRequired, -}; - -const TableWrapper = styled.div` - display: flex; - flex-direction: column; - flex: 1; - height: 100%; -`; - -const TableStyles = styled.div` - table { - background-color: ${({ theme }) => theme.colors.grayscale.light4}; - } - - .table > thead > tr > th { - border-bottom: ${({ theme }) => theme.gridUnit / 2}px solid - ${({ theme }) => theme.colors.grayscale.light2}; - background: ${({ theme }) => theme.colors.grayscale.light4}; - } -`; - -const StyledTableStylesContainer = styled.div` - overflow: auto; -`; - -class QuerySearch extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - databaseId: null, - userId: null, - searchText: null, - from: '28 days ago', - to: 'now', - status: 'success', - queriesArray: [], - queriesLoading: true, - }; - this.userMutator = this.userMutator.bind(this); - this.changeUser = this.changeUser.bind(this); - this.dbMutator = this.dbMutator.bind(this); - this.onChange = this.onChange.bind(this); - this.changeSearch = this.changeSearch.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.changeFrom = this.changeFrom.bind(this); - this.changeTo = this.changeTo.bind(this); - this.changeStatus = this.changeStatus.bind(this); - this.refreshQueries = this.refreshQueries.bind(this); - this.onUserClicked = this.onUserClicked.bind(this); - this.onDbClicked = this.onDbClicked.bind(this); - } - - componentDidMount() { - this.refreshQueries(); - } - - onUserClicked(userId) { - this.setState({ userId }, () => { - this.refreshQueries(); - }); - } - - onDbClicked(dbId) { - this.setState({ databaseId: dbId }, () => { - this.refreshQueries(); - }); - } - - onChange(db) { - const val = db ? db.value : null; - this.setState({ databaseId: val }); - } - - onKeyDown(event) { - if (event.keyCode === 13) { - this.refreshQueries(); - } - } - - getTimeFromSelection(selection) { - switch (selection) { - case 'now': - return now(); - case '1 hour ago': - return epochTimeXHoursAgo(1); - case '1 day ago': - return epochTimeXDaysAgo(1); - case '7 days ago': - return epochTimeXDaysAgo(7); - case '28 days ago': - return epochTimeXDaysAgo(28); - case '90 days ago': - return epochTimeXDaysAgo(90); - case '1 year ago': - return epochTimeXYearsAgo(1); - default: - return null; - } - } - - changeFrom(user) { - const val = user ? user.value : null; - this.setState({ from: val }); - } - - changeTo(status) { - const val = status ? status.value : null; - this.setState({ to: val }); - } - - changeUser(user) { - const val = user ? user.value : null; - this.setState({ userId: val }); - } - - insertParams(baseUrl, params) { - const validParams = params.filter(function (p) { - return p !== ''; - }); - return `${baseUrl}?${validParams.join('&')}`; - } - - changeStatus(status) { - const val = status ? status.value : null; - this.setState({ status: val }); - } - - changeSearch(event) { - this.setState({ searchText: event.target.value }); - } - - userLabel(user) { - if (user.first_name && user.last_name) { - return `${user.first_name} ${user.last_name}`; - } - return user.username; - } - - userMutator(data) { - return data.result.map(({ value, text }) => ({ - label: text, - value, - })); - } - - dbMutator(data) { - const options = data.result.map(db => ({ - value: db.id, - label: db.database_name, - })); - this.props.actions.setDatabases(data.result); - if (data.result.length === 0) { - this.props.actions.addDangerToast( - t("It seems you don't have access to any database"), - ); - } - return options; - } - - refreshQueries() { - this.setState({ queriesLoading: true }); - const params = [ - this.state.userId ? `user_id=${this.state.userId}` : '', - this.state.databaseId ? `database_id=${this.state.databaseId}` : '', - this.state.searchText ? `search_text=${this.state.searchText}` : '', - this.state.status ? `status=${this.state.status}` : '', - this.state.from - ? `from=${this.getTimeFromSelection(this.state.from)}` - : '', - this.state.to ? `to=${this.getTimeFromSelection(this.state.to)}` : '', - ]; - - SupersetClient.get({ - endpoint: this.insertParams('/superset/search_queries', params), - }) - .then(({ json }) => { - this.setState({ queriesArray: json, queriesLoading: false }); - }) - .catch(() => { - this.props.actions.addDangerToast( - t('An error occurred when refreshing queries'), - ); - }); - } - - render() { - return ( - -
-
- -
-
- -
-
- -
-
- ({ value: xt, label: xt }))} - value={this.state.to} - autosize={false} - onChange={this.changeTo} - /> - - +
+
+ ({ value: xt, label: xt }))} + value={(to as unknown) as undefined} + autosize={false} + onChange={(selected: any) => setTo(selected?.value)} + /> + +