airbnb/caravel

View on GitHub
superset-frontend/src/SqlLab/reducers/sqlLab.test.js

Summary

Maintainability
F
3 days
Test Coverage
/**
 * 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 { QueryState } from '@superset-ui/core';
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
import * as actions from 'src/SqlLab/actions/sqlLab';
import { table, initialState as mockState } from '../fixtures';

const initialState = mockState.sqlLab;

describe('sqlLabReducer', () => {
  describe('Query editors actions', () => {
    let newState;
    let defaultQueryEditor;
    let qe;
    beforeEach(() => {
      newState = { ...initialState };
      defaultQueryEditor = newState.queryEditors[0];
      const action = {
        type: actions.ADD_QUERY_EDITOR,
        queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
      };
      newState = sqlLabReducer(newState, action);
      qe = newState.queryEditors.find(e => e.id === 'abcd');
    });
    it('should add a query editor', () => {
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length + 1,
      );
    });
    it('should merge the current unsaved changes when adding a query editor', () => {
      const expectedTitle = 'new updated title';
      const updateAction = {
        type: actions.QUERY_EDITOR_SET_TITLE,
        queryEditor: initialState.queryEditors[0],
        name: expectedTitle,
      };
      newState = sqlLabReducer(newState, updateAction);
      const addAction = {
        type: actions.ADD_QUERY_EDITOR,
        queryEditor: { ...initialState.queryEditors[0], id: 'efgh' },
      };
      newState = sqlLabReducer(newState, addAction);

      expect(newState.queryEditors[0].name).toEqual(expectedTitle);
      expect(
        newState.queryEditors[newState.queryEditors.length - 1].id,
      ).toEqual('efgh');
    });
    it('should remove a query editor', () => {
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length + 1,
      );
      const action = {
        type: actions.REMOVE_QUERY_EDITOR,
        queryEditor: qe,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length,
      );
    });
    it('should select the latest query editor when tabHistory is empty', () => {
      const currentQE = newState.queryEditors[0];
      newState = {
        ...initialState,
        tabHistory: [initialState.queryEditors[0]],
      };
      const action = {
        type: actions.REMOVE_QUERY_EDITOR,
        queryEditor: currentQE,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length - 1,
      );
      expect(newState.queryEditors.map(qe => qe.id)).not.toContainEqual(
        currentQE.id,
      );
      expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]);
    });
    it('should remove a query editor including unsaved changes', () => {
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length + 1,
      );
      let action = {
        type: actions.QUERY_EDITOR_SETDB,
        queryEditor: qe,
        dbId: 123,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.dbId).toEqual(action.dbId);
      action = {
        type: actions.REMOVE_QUERY_EDITOR,
        queryEditor: qe,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.queryEditors).toHaveLength(
        initialState.queryEditors.length,
      );
      expect(newState.unsavedQueryEditor.dbId).toBeUndefined();
      expect(newState.unsavedQueryEditor.id).toBeUndefined();
    });
    it('should set q query editor active', () => {
      const expectedTitle = 'new updated title';
      const addQueryEditorAction = {
        type: actions.ADD_QUERY_EDITOR,
        queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
      };
      newState = sqlLabReducer(newState, addQueryEditorAction);
      const updateAction = {
        type: actions.QUERY_EDITOR_SET_TITLE,
        queryEditor: initialState.queryEditors[1],
        name: expectedTitle,
      };
      newState = sqlLabReducer(newState, updateAction);
      const setActiveQueryEditorAction = {
        type: actions.SET_ACTIVE_QUERY_EDITOR,
        queryEditor: defaultQueryEditor,
      };
      newState = sqlLabReducer(newState, setActiveQueryEditorAction);
      expect(newState.tabHistory[newState.tabHistory.length - 1]).toBe(
        defaultQueryEditor.id,
      );
      expect(newState.queryEditors[1].name).toEqual(expectedTitle);
    });
    it('should not fail while setting DB', () => {
      const dbId = 9;
      const action = {
        type: actions.QUERY_EDITOR_SETDB,
        queryEditor: qe,
        dbId,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not fail while setting schema', () => {
      const schema = 'foo';
      const action = {
        type: actions.QUERY_EDITOR_SET_SCHEMA,
        queryEditor: qe,
        schema,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.schema).toBe(schema);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not fail while setting autorun', () => {
      const action = {
        type: actions.QUERY_EDITOR_SET_AUTORUN,
        queryEditor: qe,
      };
      newState = sqlLabReducer(newState, { ...action, autorun: false });
      expect(newState.unsavedQueryEditor.autorun).toBe(false);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
      newState = sqlLabReducer(newState, { ...action, autorun: true });
      expect(newState.unsavedQueryEditor.autorun).toBe(true);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not fail while setting title', () => {
      const title = 'Untitled Query 1';
      const action = {
        type: actions.QUERY_EDITOR_SET_TITLE,
        queryEditor: qe,
        name: title,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.name).toBe(title);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not fail while setting Sql', () => {
      const sql = 'SELECT nothing from dev_null';
      const action = {
        type: actions.QUERY_EDITOR_SET_SQL,
        queryEditor: qe,
        sql,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.sql).toBe(sql);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not fail while setting queryLimit', () => {
      const queryLimit = 101;
      const action = {
        type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
        queryEditor: qe,
        queryLimit,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should set selectedText', () => {
      const selectedText = 'TEST';
      const action = {
        type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
        queryEditor: qe,
        sql: selectedText,
      };
      expect(qe.selectedText).toBeFalsy();
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
    });
    it('should not wiped out unsaved changes while delayed async call intercepted', () => {
      const expectedSql = 'Updated SQL WORKING IN PROGRESS--';
      const action = {
        type: actions.QUERY_EDITOR_SET_SQL,
        queryEditor: qe,
        sql: expectedSql,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.unsavedQueryEditor.sql).toBe(expectedSql);
      const interceptedAction = {
        type: actions.QUERY_EDITOR_PERSIST_HEIGHT,
        queryEditor: newState.queryEditors[0],
        northPercent: 46,
        southPercent: 54,
      };
      newState = sqlLabReducer(newState, interceptedAction);
      expect(newState.unsavedQueryEditor.sql).toBe(expectedSql);
      expect(newState.queryEditors[0].northPercent).toBe(
        interceptedAction.northPercent,
      );
    });
    it('should migrate query editor by new query editor id', () => {
      const index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
      const newQueryEditor = {
        ...qe,
        id: 'updatedNewId',
        schema: 'updatedSchema',
      };
      const action = {
        type: actions.MIGRATE_QUERY_EDITOR,
        oldQueryEditor: qe,
        newQueryEditor,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.queryEditors[index].id).toEqual('updatedNewId');
      expect(newState.queryEditors[index]).toEqual(newQueryEditor);
    });
    it('should migrate tab history by new query editor id', () => {
      expect(newState.tabHistory).toContain(qe.id);
      const action = {
        type: actions.MIGRATE_TAB_HISTORY,
        oldId: qe.id,
        newId: 'updatedNewId',
      };
      newState = sqlLabReducer(newState, action);

      expect(newState.tabHistory).toContain('updatedNewId');
      expect(newState.tabHistory).not.toContain(qe.id);
    });
    it('should clear the destroyed query editors', () => {
      const expectedQEId = '1233289';
      const action = {
        type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
        queryEditorId: expectedQEId,
      };
      newState = sqlLabReducer(
        {
          ...newState,
          destroyedQueryEditors: {
            [expectedQEId]: Date.now(),
          },
        },
        action,
      );
      expect(newState.destroyedQueryEditors).toEqual({});
    });
  });
  describe('Tables', () => {
    let newState;
    let newTable;
    beforeEach(() => {
      newTable = { ...table };
      const action = {
        type: actions.MERGE_TABLE,
        table: newTable,
      };
      newState = sqlLabReducer(initialState, action);
      newTable = newState.tables[0];
    });
    it('should add a table', () => {
      // Testing that beforeEach actually added the table
      expect(newState.tables).toHaveLength(1);
      expect(newState.tables[0].expanded).toBe(true);
    });
    it('should merge the table attributes', () => {
      // Merging the extra attribute
      newTable.extra = true;
      const action = {
        type: actions.MERGE_TABLE,
        table: newTable,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.tables).toHaveLength(1);
      expect(newState.tables[0].extra).toBe(true);
    });
    it('should overwrite table ID be ignored when the existing table is already initialized', () => {
      const action = {
        type: actions.MERGE_TABLE,
        table: newTable,
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.tables).toHaveLength(1);
      // Merging the initialized remote id
      const remoteId = 1;
      const syncAction = {
        type: actions.MERGE_TABLE,
        table: {
          ...newTable,
          id: remoteId,
          initialized: true,
        },
      };
      newState = sqlLabReducer(newState, syncAction);
      expect(newState.tables).toHaveLength(1);
      expect(newState.tables[0].initialized).toBe(true);
      expect(newState.tables[0].id).toBe(remoteId);
      const overwriteAction = {
        type: actions.MERGE_TABLE,
        table: {
          id: 'rnd_new_id',
          ...newTable,
        },
      };
      newState = sqlLabReducer(newState, overwriteAction);
      expect(newState.tables).toHaveLength(1);
      expect(newState.tables[0].id).toBe(remoteId);
    });
    it('should expand and collapse a table', () => {
      const collapseTableAction = {
        type: actions.COLLAPSE_TABLE,
        table: newTable,
      };
      newState = sqlLabReducer(newState, collapseTableAction);
      expect(newState.tables[0].expanded).toBe(false);
      const expandTableAction = {
        type: actions.EXPAND_TABLE,
        table: newTable,
      };
      newState = sqlLabReducer(newState, expandTableAction);
      expect(newState.tables[0].expanded).toBe(true);
    });
    it('should remove a table', () => {
      const action = {
        type: actions.REMOVE_TABLES,
        tables: [newTable],
      };
      newState = sqlLabReducer(newState, action);
      expect(newState.tables).toHaveLength(0);
    });
  });
  describe('Run Query', () => {
    const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439';
    const CHANGED_ON_TIMESTAMP = 1687765985439;
    let newState;
    let query;
    beforeEach(() => {
      newState = { ...initialState };
      query = {
        id: 'abcd',
        progress: 0,
        changed_on: DENORMALIZED_CHANGED_ON,
        startDttm: CHANGED_ON_TIMESTAMP,
        state: 'running',
        cached: false,
        sqlEditorId: 'dfsadfs',
      };
    });
    it('should start a query', () => {
      const action = {
        type: actions.START_QUERY,
        query: {
          id: 'abcd',
          progress: 0,
          changed_on: DENORMALIZED_CHANGED_ON,
          startDttm: CHANGED_ON_TIMESTAMP,
          state: 'running',
          cached: false,
          sqlEditorId: 'dfsadfs',
        },
      };
      newState = sqlLabReducer(newState, action);
      expect(Object.keys(newState.queries)).toHaveLength(1);
    });
    it('should stop the query', () => {
      const startQueryAction = {
        type: actions.START_QUERY,
        query,
      };
      newState = sqlLabReducer(newState, startQueryAction);
      const stopQueryAction = {
        type: actions.STOP_QUERY,
        query,
      };
      newState = sqlLabReducer(newState, stopQueryAction);
      const q = newState.queries[Object.keys(newState.queries)[0]];
      expect(q.state).toBe('stopped');
    });
    it('should remove a query', () => {
      const startQueryAction = {
        type: actions.START_QUERY,
        query,
      };
      newState = sqlLabReducer(newState, startQueryAction);
      const removeQueryAction = {
        type: actions.REMOVE_QUERY,
        query,
      };
      newState = sqlLabReducer(newState, removeQueryAction);
      expect(Object.keys(newState.queries)).toHaveLength(0);
    });
    it('should refresh queries when polling returns new results', () => {
      const startDttmInStr = '1693433503447.166992';
      const endDttmInStr = '1693433503500.23132';
      newState = sqlLabReducer(
        {
          ...newState,
          queries: { abcd: {} },
        },
        actions.refreshQueries({
          abcd: {
            ...query,
            startDttm: startDttmInStr,
            endDttm: endDttmInStr,
          },
        }),
      );
      expect(newState.queries.abcd.changed_on).toBe(DENORMALIZED_CHANGED_ON);
      expect(newState.queries.abcd.startDttm).toBe(Number(startDttmInStr));
      expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr));
      expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP);
    });
    it('should skip refreshing queries when polling contains existing results', () => {
      const completedQuery = {
        ...query,
        extra: {
          columns: [],
          progress: null,
        },
      };
      newState = sqlLabReducer(
        {
          ...newState,
          queries: { abcd: query, def: completedQuery },
        },
        actions.refreshQueries({
          abcd: {
            ...query,
          },
          def: {
            ...completedQuery,
            extra: {
              columns: [],
              progress: null,
            },
          },
        }),
      );
      expect(newState.queries.abcd).toBe(query);
      expect(newState.queries.def).toBe(completedQuery);
    });
    it('should refresh queries when polling returns empty', () => {
      newState = sqlLabReducer(newState, actions.refreshQueries({}));
    });
  });
  describe('CLEAR_INACTIVE_QUERIES', () => {
    let newState;
    let query;
    beforeEach(() => {
      query = {
        id: 'abcd',
        changed_on: Date.now(),
        startDttm: Date.now(),
        state: QueryState.Fetching,
        progress: 100,
        resultsKey: 'fa3dccc4-c549-4fbf-93c8-b4fb5a6fb8b7',
        cached: false,
      };
    });
    it('updates queries that have already been completed', () => {
      newState = sqlLabReducer(
        {
          ...newState,
          queries: {
            abcd: {
              ...query,
              results: {
                query_id: 1234,
                status: QueryState.Success,
                data: [],
              },
            },
          },
        },
        actions.clearInactiveQueries(Date.now()),
      );
      expect(newState.queries.abcd.state).toBe(QueryState.Success);
    });
  });
});