michael-s-molina commented on code in PR #24539:
URL: https://github.com/apache/superset/pull/24539#discussion_r1260318616


##########
superset-frontend/src/SqlLab/App.jsx:
##########
@@ -56,6 +60,37 @@ const sqlLabPersistStateConfig = {
     slicer: paths => state => {
       const subset = {};
       paths.forEach(path => {
+        if (path === 'sqlLab.unsavedQueryEditor') {
+          const {
+            queryEditors,
+            editorTabLastUpdatedAt,
+            unsavedQueryEditor,
+            tables,
+            queries,
+            tabHistory,
+          } = state.sqlLab;
+          const unsavedQueryEditors = filterUnsavedQueryEditorList(
+            queryEditors,
+            unsavedQueryEditor,
+            editorTabLastUpdatedAt,
+          );
+          const hasFinishedMigrationFromLocalStorage =
+            unsavedQueryEditors.every(({ inLocalStorage }) => !inLocalStorage);
+          if (unsavedQueryEditors.length > 0) {

Review Comment:
   ```suggestion
             if (unsavedQueryEditors.length > 0) {
               const hasFinishedMigrationFromLocalStorage =
                 unsavedQueryEditors.every(
                   ({ inLocalStorage }) => !inLocalStorage,
                 );
   ```



##########
superset-frontend/src/SqlLab/App.jsx:
##########
@@ -56,6 +60,37 @@ const sqlLabPersistStateConfig = {
     slicer: paths => state => {
       const subset = {};
       paths.forEach(path => {
+        if (path === 'sqlLab.unsavedQueryEditor') {

Review Comment:
   Could you extract a constant with `'sqlLab.unsavedQueryEditor'`?



##########
superset-frontend/src/SqlLab/actions/sqlLab.js:
##########
@@ -1037,58 +885,18 @@ export function 
queryEditorSetAndSaveSql(targetQueryEditor, sql) {
 }
 
 export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
-  return function (dispatch) {
-    const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
-      ? SupersetClient.put({
-          endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
-          postPayload: { query_limit: queryLimit },
-        })
-      : Promise.resolve();
-
-    return sync
-      .then(() =>
-        dispatch({
-          type: QUERY_EDITOR_SET_QUERY_LIMIT,
-          queryEditor,
-          queryLimit,
-        }),
-      )
-      .catch(() =>
-        dispatch(
-          addDangerToast(
-            t(
-              'An error occurred while setting the tab name. Please contact 
your administrator.',
-            ),
-          ),
-        ),
-      );
+  return {
+    type: QUERY_EDITOR_SET_QUERY_LIMIT,
+    queryEditor,
+    queryLimit,
   };
 }
 
 export function queryEditorSetTemplateParams(queryEditor, templateParams) {
-  return function (dispatch) {
-    dispatch({
-      type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
-      queryEditor,
-      templateParams,
-    });
-    const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
-      ? SupersetClient.put({
-          endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
-          postPayload: { template_params: templateParams },
-        })
-      : Promise.resolve();
-
-    return sync.catch(() =>
-      dispatch(
-        addDangerToast(
-          t(
-            'An error occurred while setting the tab template parameters. ' +
-              'Please contact your administrator.',
-          ),
-        ),
-      ),
-    );
+  return {

Review Comment:
   Nice cleanup! A lot of duplicated logic in the old actions!



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx:
##########
@@ -0,0 +1,120 @@
+/**
+ * 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, { useRef, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { FeatureFlag, t } from '@superset-ui/core';
+import {
+  SqlLabRootState,
+  QueryEditor,
+  UnsavedQueryEditor,
+} from 'src/SqlLab/types';
+import { isFeatureEnabled } from 'src/featureFlags';
+import { useUpdateSqlEditorTabMutation } from 
'src/hooks/apiResources/sqlEditorTabs';
+import { useDebounceValue } from 'src/hooks/useDebounceValue';
+import {
+  addDangerToast,
+  setEditorTabLastUpdate,
+} from 'src/SqlLab/actions/sqlLab';
+
+const INTERVAL = 5000;
+
+function hasUnsavedChanges(
+  queryEditor: QueryEditor,
+  lastSavedDateTime: number,
+) {
+  return (
+    queryEditor.inLocalStorage ||
+    (queryEditor.updatedAt && queryEditor.updatedAt > lastSavedDateTime)
+  );
+}
+
+export function filterUnsavedQueryEditorList(
+  queryEditors: QueryEditor[],
+  unsavedQueryEditor: UnsavedQueryEditor,
+  lastSavedDateTime: number,
+) {
+  return queryEditors
+    .map(queryEditor => ({
+      ...queryEditor,
+      ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
+    }))
+    .filter(queryEditor => hasUnsavedChanges(queryEditor, lastSavedDateTime));
+}
+
+const EditorAutoSync: React.FC = () => {
+  const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
+    state => state.sqlLab.queryEditors,
+  );
+  const unsavedQueryEditor = useSelector<SqlLabRootState, UnsavedQueryEditor>(
+    state => state.sqlLab.unsavedQueryEditor,
+  );
+  const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
+    state => state.sqlLab.editorTabLastUpdatedAt,
+  );
+  const dispatch = useDispatch();
+  const lastSavedDateTimeRef = useRef<number>(editorTabLastUpdatedAt);
+  const [updateSqlEditor, { isError }] = useUpdateSqlEditorTabMutation();
+
+  const debouncedUnsavedQueryEditor = useDebounceValue(
+    unsavedQueryEditor,
+    INTERVAL,
+  );
+
+  useEffect(() => {
+    if (!isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
+      return;
+    }
+    const unsaved = filterUnsavedQueryEditorList(
+      queryEditors,
+      debouncedUnsavedQueryEditor,
+      lastSavedDateTimeRef.current,
+    );
+
+    Promise.all(
+      unsaved
+        // TODO: Migrate migrateQueryEditorFromLocalStorage
+        //       in TabbedSqlEditors logic by addSqlEditor mutation later
+        .filter(({ inLocalStorage }) => !inLocalStorage)
+        .map(queryEditor => updateSqlEditor({ queryEditor })),
+    ).then(resolvers => {
+      if (!resolvers.some(result => 'error' in result)) {
+        lastSavedDateTimeRef.current = Date.now();
+        dispatch(setEditorTabLastUpdate(lastSavedDateTimeRef.current));
+      }
+    });
+  }, [debouncedUnsavedQueryEditor, dispatch, queryEditors, updateSqlEditor]);
+
+  useEffect(() => {
+    if (isError) {
+      dispatch(
+        addDangerToast(
+          t(
+            'An error occurred while storing your query in the backend. ' +

Review Comment:
   ```suggestion
               'An error occurred while saving your editor state. ' +
   ```



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx:
##########
@@ -0,0 +1,178 @@
+/**
+ * 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.
+ */
+/**
+ * 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 fetchMock from 'fetch-mock';
+import * as featureFlags from 'src/featureFlags';
+import { FeatureFlag } from '@superset-ui/core';
+import { render, act } from 'spec/helpers/testing-library';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import EditorAutoSync from '.';
+
+const editorTabLastUpdatedAt = Date.now();
+const unsavedSqlLabState = {
+  ...initialState.sqlLab,
+  unsavedQueryEditor: {
+    id: defaultQueryEditor.id,
+    name: 'updated tab name',
+    updatedAt: editorTabLastUpdatedAt + 100,
+  },
+  editorTabLastUpdatedAt,
+};
+beforeAll(() => {
+  jest.useFakeTimers();
+});
+
+afterAll(() => {
+  jest.useRealTimers();
+});
+
+test('sync the unsaved editor tab state with api when unsaved has made since 
lastest updated', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, 200);
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: unsavedSqlLabState,
+    },
+  });
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('skip syncing the unsaved editor tab state when the updates already 
synced', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, 200);
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: {
+        ...initialState.sqlLab,
+        unsavedQueryEditor: {
+          id: defaultQueryEditor.id,
+          name: 'updated tab name',
+          updatedAt: editorTabLastUpdatedAt - 100,
+        },
+        editorTabLastUpdatedAt,
+      },
+    },
+  });
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('renders an error toast when api update failed', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, {
+    throws: new Error('errorMessage'),
+  });
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  const { findByText } = render(
+    <>
+      <EditorAutoSync />
+      <ToastContainer />
+    </>,
+    {
+      useRedux: true,
+      initialState: {
+        ...initialState,
+        sqlLab: unsavedSqlLabState,
+      },
+    },
+  );
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  const errorToast = await findByText(
+    'An error occurred while storing your query in the backend. ' +
+      'Please contact your administrator if this problem persists.',
+  );
+  expect(errorToast).toBeTruthy();
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('skip syncing the unsaved editor tab state with api when 
SQLLAB_BACKEND_PERSISTENCE is off', async () => {

Review Comment:
   ```suggestion
   test('skip syncing the unsaved editor tab state when 
SQLLAB_BACKEND_PERSISTENCE is off', async () => {
   ```



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx:
##########
@@ -0,0 +1,178 @@
+/**
+ * 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.
+ */
+/**
+ * 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 fetchMock from 'fetch-mock';
+import * as featureFlags from 'src/featureFlags';
+import { FeatureFlag } from '@superset-ui/core';
+import { render, act } from 'spec/helpers/testing-library';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import EditorAutoSync from '.';
+
+const editorTabLastUpdatedAt = Date.now();
+const unsavedSqlLabState = {
+  ...initialState.sqlLab,
+  unsavedQueryEditor: {
+    id: defaultQueryEditor.id,
+    name: 'updated tab name',
+    updatedAt: editorTabLastUpdatedAt + 100,
+  },
+  editorTabLastUpdatedAt,
+};
+beforeAll(() => {
+  jest.useFakeTimers();
+});
+
+afterAll(() => {
+  jest.useRealTimers();
+});
+
+test('sync the unsaved editor tab state with api when unsaved has made since 
lastest updated', async () => {

Review Comment:
   ```suggestion
   test('sync the unsaved editor tab state when there are new changes since the 
last update', async () => {
   ```



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx:
##########
@@ -0,0 +1,120 @@
+/**
+ * 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, { useRef, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { FeatureFlag, t } from '@superset-ui/core';
+import {
+  SqlLabRootState,
+  QueryEditor,
+  UnsavedQueryEditor,
+} from 'src/SqlLab/types';
+import { isFeatureEnabled } from 'src/featureFlags';
+import { useUpdateSqlEditorTabMutation } from 
'src/hooks/apiResources/sqlEditorTabs';
+import { useDebounceValue } from 'src/hooks/useDebounceValue';
+import {
+  addDangerToast,
+  setEditorTabLastUpdate,
+} from 'src/SqlLab/actions/sqlLab';
+
+const INTERVAL = 5000;
+
+function hasUnsavedChanges(
+  queryEditor: QueryEditor,
+  lastSavedDateTime: number,

Review Comment:
   Maybe rename all occurrences of `lastSavedDateTime` to `lastSavedTimestamp`?



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx:
##########
@@ -0,0 +1,178 @@
+/**
+ * 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.
+ */
+/**
+ * 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 fetchMock from 'fetch-mock';
+import * as featureFlags from 'src/featureFlags';
+import { FeatureFlag } from '@superset-ui/core';
+import { render, act } from 'spec/helpers/testing-library';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import EditorAutoSync from '.';
+
+const editorTabLastUpdatedAt = Date.now();
+const unsavedSqlLabState = {
+  ...initialState.sqlLab,
+  unsavedQueryEditor: {
+    id: defaultQueryEditor.id,
+    name: 'updated tab name',
+    updatedAt: editorTabLastUpdatedAt + 100,
+  },
+  editorTabLastUpdatedAt,
+};
+beforeAll(() => {
+  jest.useFakeTimers();
+});
+
+afterAll(() => {
+  jest.useRealTimers();
+});
+
+test('sync the unsaved editor tab state with api when unsaved has made since 
lastest updated', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, 200);
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: unsavedSqlLabState,
+    },
+  });
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('skip syncing the unsaved editor tab state when the updates already 
synced', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, 200);
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: {
+        ...initialState.sqlLab,
+        unsavedQueryEditor: {
+          id: defaultQueryEditor.id,
+          name: 'updated tab name',
+          updatedAt: editorTabLastUpdatedAt - 100,
+        },
+        editorTabLastUpdatedAt,
+      },
+    },
+  });
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('renders an error toast when api update failed', async () => {

Review Comment:
   ```suggestion
   test('renders an error toast when the sync fails', async () => {
   ```



##########
superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx:
##########
@@ -0,0 +1,178 @@
+/**
+ * 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.
+ */
+/**
+ * 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 fetchMock from 'fetch-mock';
+import * as featureFlags from 'src/featureFlags';
+import { FeatureFlag } from '@superset-ui/core';
+import { render, act } from 'spec/helpers/testing-library';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import EditorAutoSync from '.';
+
+const editorTabLastUpdatedAt = Date.now();
+const unsavedSqlLabState = {
+  ...initialState.sqlLab,
+  unsavedQueryEditor: {
+    id: defaultQueryEditor.id,
+    name: 'updated tab name',
+    updatedAt: editorTabLastUpdatedAt + 100,
+  },
+  editorTabLastUpdatedAt,
+};
+beforeAll(() => {
+  jest.useFakeTimers();
+});
+
+afterAll(() => {
+  jest.useRealTimers();
+});
+
+test('sync the unsaved editor tab state with api when unsaved has made since 
lastest updated', async () => {
+  const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+  fetchMock.put(updateEditorTabState, 200);
+  const isFeatureEnabledMock = jest
+    .spyOn(featureFlags, 'isFeatureEnabled')
+    .mockImplementation(
+      featureFlag => featureFlag === FeatureFlag.SQLLAB_BACKEND_PERSISTENCE,
+    );
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: unsavedSqlLabState,
+    },
+  });
+  await act(async () => {
+    jest.runAllTimers();
+  });
+  expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
+  isFeatureEnabledMock.mockClear();
+  fetchMock.restore();
+});
+
+test('skip syncing the unsaved editor tab state when the updates already 
synced', async () => {

Review Comment:
   ```suggestion
   test('skip syncing the unsaved editor tab state when the updates are already 
synced', async () => {
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to