diff --git a/docs/package.json b/docs/package.json
index dd7b05dbfe66..15ff2332a931 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -67,7 +67,7 @@
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^6.2.2",
- "babel-loader": "^9.2.1",
+ "babel-loader": "^10.0.0",
"caniuse-lite": "^1.0.30001766",
"docusaurus-plugin-less": "^2.0.2",
"docusaurus-plugin-openapi-docs": "^4.6.0",
diff --git a/docs/yarn.lock b/docs/yarn.lock
index a003a7c640aa..7f5156481269 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -5386,6 +5386,13 @@ axios@^1.12.2:
form-data "^4.0.4"
proxy-from-env "^1.1.0"
+babel-loader@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-10.0.0.tgz#b9743714c0e1e084b3e4adef3cd5faee33089977"
+ integrity sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==
+ dependencies:
+ find-up "^5.0.0"
+
babel-loader@^9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b"
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index abf040df8a52..8fc4c695e8bc 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -100,6 +100,7 @@
"query-string": "6.14.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
+ "react-arborist": "^3.4.3",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^11.1.3",
@@ -190,7 +191,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-loadable": "^5.5.11",
@@ -212,7 +213,7 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
- "baseline-browser-mapping": "^2.9.18",
+ "baseline-browser-mapping": "^2.9.19",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
@@ -240,7 +241,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.15.4",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
- "fetch-mock": "^11.1.5",
+ "fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.6",
@@ -19903,9 +19904,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.0.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
- "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz",
+ "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@@ -24080,9 +24081,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -32118,25 +32119,19 @@
}
},
"node_modules/fetch-mock": {
- "version": "11.1.5",
- "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-11.1.5.tgz",
- "integrity": "sha512-KHmZDnZ1ry0pCTrX4YG5DtThHi0MH+GNI9caESnzX/nMJBrvppUHMvLx47M0WY9oAtKOMiPfZDRpxhlHg89BOA==",
+ "version": "12.6.0",
+ "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz",
+ "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/glob-to-regexp": "^0.4.4",
"dequal": "^2.0.3",
"glob-to-regexp": "^0.4.1",
- "is-subset": "^0.1.1",
"regexparam": "^3.0.0"
},
"engines": {
- "node": ">=8.0.0"
- },
- "peerDependenciesMeta": {
- "node-fetch": {
- "optional": true
- }
+ "node": ">=18.11.0"
}
},
"node_modules/fetch-retry": {
@@ -36914,7 +36909,9 @@
"resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
"integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/is-symbol": {
"version": "1.1.1",
@@ -49668,6 +49665,88 @@
"react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-arborist": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.3.tgz",
+ "integrity": "sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-dnd": "^14.0.3",
+ "react-dnd-html5-backend": "^14.0.3",
+ "react-window": "^1.8.11",
+ "redux": "^5.0.0",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.14",
+ "react-dom": ">= 16.14"
+ }
+ },
+ "node_modules/react-arborist/node_modules/dnd-core": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz",
+ "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-dnd/asap": "^4.0.0",
+ "@react-dnd/invariant": "^2.0.0",
+ "redux": "^4.1.1"
+ }
+ },
+ "node_modules/react-arborist/node_modules/dnd-core/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/react-arborist/node_modules/react-dnd": {
+ "version": "14.0.5",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
+ "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-dnd/invariant": "^2.0.0",
+ "@react-dnd/shallowequal": "^2.0.0",
+ "dnd-core": "14.0.1",
+ "fast-deep-equal": "^3.1.3",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "peerDependencies": {
+ "@types/hoist-non-react-statics": ">= 3.3.1",
+ "@types/node": ">= 12",
+ "@types/react": ">= 16",
+ "react": ">= 16.14"
+ },
+ "peerDependenciesMeta": {
+ "@types/hoist-non-react-statics": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-arborist/node_modules/react-dnd-html5-backend": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
+ "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
+ "license": "MIT",
+ "dependencies": {
+ "dnd-core": "14.0.1"
+ }
+ },
+ "node_modules/react-arborist/node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
@@ -57889,6 +57968,15 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -63675,13 +63763,13 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.23",
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
- "fetch-mock": "^11.1.4",
+ "fetch-mock": "^12.6.0",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.3.6"
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index dff5390cb384..45a5a3379f73 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -182,6 +182,7 @@
"query-string": "6.14.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
+ "react-arborist": "^3.4.3",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^11.1.3",
@@ -272,7 +273,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-loadable": "^5.5.11",
@@ -294,7 +295,7 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
- "baseline-browser-mapping": "^2.9.18",
+ "baseline-browser-mapping": "^2.9.19",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
@@ -322,7 +323,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.15.4",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
- "fetch-mock": "^11.1.5",
+ "fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.6",
diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json
index 52dc747f816a..1275e8cf1c16 100644
--- a/superset-frontend/packages/superset-ui-core/package.json
+++ b/superset-frontend/packages/superset-ui-core/package.json
@@ -78,11 +78,11 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.23",
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/prop-types": "^15.7.15",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
- "fetch-mock": "^11.1.4",
+ "fetch-mock": "^12.6.0",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.3.6"
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Button/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/Button/index.tsx
index 6d90ef2fd08d..0266680d655a 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Button/index.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Button/index.tsx
@@ -126,7 +126,7 @@ export function Button(props: ButtonProps) {
minWidth: cta ? theme.sizeUnit * 36 : undefined,
minHeight: cta ? theme.sizeUnit * 8 : undefined,
marginLeft: 0,
- '& + .superset-button': {
+ '& + .superset-button:not(.ant-btn-compact-item)': {
marginLeft: theme.sizeUnit * 2,
},
'& > span > :first-of-type': {
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
index 319227687b18..9c6db6ac6a6a 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
@@ -76,6 +76,9 @@ import {
FileOutlined,
FileTextOutlined,
FireOutlined,
+ FolderAddOutlined,
+ FolderOpenOutlined,
+ FolderOutlined,
FormOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
@@ -94,15 +97,18 @@ import {
MenuFoldOutlined,
MenuUnfoldOutlined,
MinusCircleOutlined,
+ MinusSquareOutlined,
MoonOutlined,
LoadingOutlined,
LoginOutlined,
MonitorOutlined,
MoreOutlined,
OrderedListOutlined,
+ PartitionOutlined,
PieChartOutlined,
PicCenterOutlined,
PlusCircleOutlined,
+ PlusSquareOutlined,
PlusOutlined,
ProfileOutlined,
QuestionCircleOutlined,
@@ -217,6 +223,9 @@ const AntdIcons = {
FileOutlined,
FileTextOutlined,
FireOutlined,
+ FolderAddOutlined,
+ FolderOpenOutlined,
+ FolderOutlined,
FormOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
@@ -240,13 +249,16 @@ const AntdIcons = {
MenuFoldOutlined,
MenuUnfoldOutlined,
MinusCircleOutlined,
+ MinusSquareOutlined,
MonitorOutlined,
MoonOutlined,
MoreOutlined,
OrderedListOutlined,
+ PartitionOutlined,
PieChartOutlined,
PicCenterOutlined,
PlusCircleOutlined,
+ PlusSquareOutlined,
PlusOutlined,
ProfileOutlined,
ReloadOutlined,
diff --git a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx
index abbc6c21cd5a..ad31e512f293 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ImageLoader.test.tsx
@@ -24,12 +24,18 @@ import { ImageLoader, type BackgroundPosition } from './ImageLoader';
global.URL.createObjectURL = jest.fn(() => '/local_url');
const blob = new Blob([], { type: 'image/png' });
+beforeAll(() => {
+ fetchMock.mockGlobal();
+});
+
+afterAll(() => {
+ fetchMock.hardReset();
+});
+
fetchMock.get(
- '/thumbnail',
+ 'glob:*/thumbnail',
{ body: blob, headers: { 'Content-Type': 'image/png' } },
- {
- sendAsJson: false,
- },
+ { name: 'thumbnail' },
);
describe('ImageLoader', () => {
@@ -44,7 +50,7 @@ describe('ImageLoader', () => {
return render();
};
- afterEach(() => fetchMock.resetHistory());
+ afterEach(() => fetchMock.clearHistory());
it('is a valid element', async () => {
setup();
@@ -57,7 +63,7 @@ describe('ImageLoader', () => {
'src',
'/fallback',
);
- expect(fetchMock.calls(/thumbnail/)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(/thumbnail/)).toHaveLength(1);
expect(global.URL.createObjectURL).toHaveBeenCalled();
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
@@ -66,13 +72,14 @@ describe('ImageLoader', () => {
});
it('displays fallback image when response is not an image', async () => {
- fetchMock.once('/thumbnail2', {});
- setup({ src: '/thumbnail2' });
+ fetchMock.once('glob:*/thumbnail2', {}, { name: 'thumbnail2' });
+
+ setup({ src: 'glob:*/thumbnail2' });
expect(screen.getByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',
);
- expect(fetchMock.calls(/thumbnail2/)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(/thumbnail2/)).toHaveLength(1);
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',
diff --git a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
index b1c879cc221e..17182227323a 100644
--- a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
@@ -37,6 +37,9 @@ import { SliceIdAndOrFormData } from '../../../src/chart/clients/ChartClient';
configureTranslation();
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('ChartClient', () => {
let chartClient: ChartClient;
@@ -50,7 +53,7 @@ describe('ChartClient', () => {
chartClient = new ChartClient();
});
- afterEach(() => fetchMock.restore());
+ afterEach(() => fetchMock.removeRoutes().clearHistory());
describe('new ChartClient(config)', () => {
it('creates a client without argument', () => {
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
index 7f29db6123bb..8174fb8ad433 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
@@ -21,10 +21,13 @@ import fetchMock from 'fetch-mock';
import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('SupersetClient', () => {
- beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '' }));
+ beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
- afterAll(() => fetchMock.restore());
+ afterAll(() => fetchMock.removeRoutes().clearHistory());
afterEach(() => SupersetClient.reset());
@@ -108,9 +111,11 @@ describe('SupersetClient', () => {
mockDeleteUrl,
];
networkCalls.map((url: string) =>
- expect(fetchMock.calls(url)[0][1]?.headers).toStrictEqual({
- Accept: 'application/json',
- 'X-CSRFToken': '1234',
+ expect(
+ fetchMock.callHistory.calls(url)[0].options?.headers,
+ ).toStrictEqual({
+ accept: 'application/json',
+ 'x-csrftoken': '1234',
}),
);
@@ -137,6 +142,6 @@ describe('SupersetClient', () => {
authenticatedSpy.mockRestore();
csrfSpy.mockRestore();
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
});
});
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
index 6a778cdd35d6..a5e0fe6cc669 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
@@ -20,14 +20,15 @@ import fetchMock from 'fetch-mock';
import { SupersetClientClass, ClientConfig, CallApi } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('SupersetClientClass', () => {
beforeEach(() => {
- fetchMock.reset();
- fetchMock.get(LOGIN_GLOB, { result: '' });
+ fetchMock.clearHistory().removeRoutes();
+ fetchMock.get(LOGIN_GLOB, { result: '' }, { name: LOGIN_GLOB });
});
- afterAll(() => fetchMock.restore());
-
describe('new SupersetClientClass()', () => {
it('fallback protocol to https when setting only host', () => {
const client = new SupersetClientClass({ host: 'TEST-HOST' });
@@ -89,21 +90,22 @@ describe('SupersetClientClass', () => {
});
describe('.init()', () => {
- beforeEach(() =>
- fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }),
- );
- afterEach(() => fetchMock.reset());
+ beforeEach(() => {
+ fetchMock.removeRoute(LOGIN_GLOB);
+ fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB });
+ });
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
it('calls api/v1/security/csrf_token/ when init() is called if no CSRF token is passed', async () => {
- expect.assertions(1);
+ // expect.assertions(1);
await new SupersetClientClass().init();
- expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1);
});
it('does NOT call api/v1/security/csrf_token/ when init() is called if a CSRF token is passed', async () => {
expect.assertions(1);
await new SupersetClientClass({ csrfToken: 'abc' }).init();
- expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
});
it('calls api/v1/security/csrf_token/ when init(force=true) is called even if a CSRF token is passed', async () => {
@@ -112,20 +114,19 @@ describe('SupersetClientClass', () => {
const client = new SupersetClientClass({ csrfToken: initialToken });
await client.init();
- expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
expect(client.csrfToken).toBe(initialToken);
await client.init(true);
- expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1);
expect(client.csrfToken).not.toBe(initialToken);
});
it('throws if api/v1/security/csrf_token/ returns an error', async () => {
expect.assertions(1);
const rejectError = { status: 403 };
- fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectError), {
- overwriteRoutes: true,
- });
+ fetchMock.removeRoute(LOGIN_GLOB);
+ fetchMock.get(LOGIN_GLOB, { throws: rejectError }, { name: LOGIN_GLOB });
let error;
try {
@@ -141,7 +142,7 @@ describe('SupersetClientClass', () => {
it('throws if api/v1/security/csrf_token/ does not return a token', async () => {
expect.assertions(1);
- fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true });
+ fetchMock.modifyRoute(LOGIN_GLOB, { response: {} });
let error;
try {
@@ -157,9 +158,8 @@ describe('SupersetClientClass', () => {
it('does not set csrfToken if response is not json', async () => {
expect.assertions(1);
- fetchMock.get(LOGIN_GLOB, '123', {
- overwriteRoutes: true,
- });
+ fetchMock.removeRoute(LOGIN_GLOB);
+ fetchMock.get(LOGIN_GLOB, { response: '123' }, { name: LOGIN_GLOB });
let error;
try {
@@ -175,7 +175,7 @@ describe('SupersetClientClass', () => {
});
describe('.isAuthenticated()', () => {
- afterEach(() => fetchMock.reset());
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns true if there is a token and false if not', async () => {
expect.assertions(2);
@@ -227,9 +227,8 @@ describe('SupersetClientClass', () => {
expect.assertions(4);
const rejectValue = { status: 403 };
- fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), {
- overwriteRoutes: true,
- });
+ fetchMock.removeRoutes();
+ fetchMock.get(LOGIN_GLOB, { throws: rejectValue }, { name: LOGIN_GLOB });
const client = new SupersetClientClass({});
let error;
@@ -253,18 +252,19 @@ describe('SupersetClientClass', () => {
}
// reset
+ fetchMock.removeRoutes();
fetchMock.get(
LOGIN_GLOB,
{ result: 1234 },
{
- overwriteRoutes: true,
+ name: LOGIN_GLOB,
},
);
});
});
describe('requests', () => {
- afterEach(() => fetchMock.restore());
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
const protocol = 'https:';
const host = 'host';
@@ -306,11 +306,11 @@ describe('SupersetClientClass', () => {
await client.delete({ url: mockDeleteUrl });
await client.request({ url: mockRequestUrl, method: 'DELETE' });
- expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockDeleteUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockRequestUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockDeleteUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockRequestUrl)).toHaveLength(1);
expect(authSpy).toHaveBeenCalledTimes(5);
authSpy.mockRestore();
@@ -331,7 +331,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
- const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
+ .options as CallApi;
expect(fetchRequest.mode).toBe(clientConfig.mode);
expect(fetchRequest.credentials).toBe(clientConfig.credentials);
expect(fetchRequest.headers).toEqual(
@@ -354,10 +355,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
- const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
+ .options as CallApi;
expect(fetchRequest.headers).toEqual(
expect.objectContaining({
- guestTokenHeader: 'abc123',
+ guesttokenheader: 'abc123',
}),
);
});
@@ -370,10 +372,10 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
- expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
await client.get({ endpoint: mockGetEndpoint });
- expect(fetchMock.calls(mockGetUrl)).toHaveLength(2);
+ expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(2);
});
it('supports parsing a response as text', async () => {
@@ -384,7 +386,7 @@ describe('SupersetClientClass', () => {
url: mockTextUrl,
parseMethod: 'text',
});
- expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(text).toBe(mockTextJsonResponse);
});
@@ -409,7 +411,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl, ...overrideConfig });
- const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
+ .options as CallApi;
expect(fetchRequest.mode).toBe(overrideConfig.mode);
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
expect(fetchRequest.headers).toEqual(
@@ -428,10 +431,10 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl });
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
await client.post({ endpoint: mockPostEndpoint });
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(2);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(2);
});
it('allows overriding host, headers, mode, and credentials per-request', async () => {
@@ -454,7 +457,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, ...overrideConfig });
- const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
+ .options as CallApi;
expect(fetchRequest.mode).toBe(overrideConfig.mode);
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
@@ -473,7 +477,7 @@ describe('SupersetClientClass', () => {
url: mockTextUrl,
parseMethod: 'text',
});
- expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(text).toBe(mockTextJsonResponse);
});
@@ -485,10 +489,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, postPayload });
- const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
+ .options as CallApi;
const formData = fetchRequest.body as FormData;
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
Object.entries(postPayload).forEach(([key, value]) => {
expect(formData.get(key)).toBe(JSON.stringify(value));
});
@@ -502,10 +507,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, postPayload, stringify: false });
- const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
+ const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
+ .options as CallApi;
const formData = fetchRequest.body as FormData;
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
Object.entries(postPayload).forEach(([key, value]) => {
expect(formData.get(key)).toBe(String(value));
});
@@ -528,6 +534,7 @@ describe('SupersetClientClass', () => {
// @ts-ignore
window.location = {
pathname: mockRequestPath,
+ // @ts-ignore
search: mockRequestSearch,
href: mockHref,
};
@@ -535,9 +542,7 @@ describe('SupersetClientClass', () => {
.spyOn(SupersetClientClass.prototype, 'ensureAuth')
.mockImplementation();
const rejectValue = { status: 401 };
- fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue), {
- overwriteRoutes: true,
- });
+ fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue));
});
afterEach(() => {
@@ -563,10 +568,11 @@ describe('SupersetClientClass', () => {
it('should not redirect again if already on login page', async () => {
const client = new SupersetClientClass({});
- // @ts-expect-error
+ // @ts-ignore
window.location = {
href: '/login?next=something',
pathname: '/login',
+ // @ts-ignore
search: '?next=something',
};
@@ -636,7 +642,8 @@ describe('SupersetClientClass', () => {
let createElement: any;
beforeEach(async () => {
- fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
+ fetchMock.removeRoute(LOGIN_GLOB);
+ fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB });
client = new SupersetClientClass({ protocol, host });
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts
index 387b96575be5..35e8ea24fdbb 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApi.test.ts
@@ -29,14 +29,17 @@ const corruptObject = new BadObject();
/* @ts-expect-error */
BadObject.prototype.toString = undefined;
-const mockGetUrl = '/mock/get/url';
-const mockPostUrl = '/mock/post/url';
-const mockPutUrl = '/mock/put/url';
-const mockPatchUrl = '/mock/patch/url';
-const mockCacheUrl = '/mock/cache/url';
-const mockNotFound = '/mock/notfound';
-const mockErrorUrl = '/mock/error/url';
-const mock503 = '/mock/503';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
+const mockGetUrl = 'glob:*/mock/get/url';
+const mockPostUrl = 'glob:*/mock/post/url';
+const mockPutUrl = 'glob:*/mock/put/url';
+const mockPatchUrl = 'glob:*/mock/patch/url';
+const mockCacheUrl = 'glob:*/mock/cache/url';
+const mockNotFound = 'glob:*/mock/notfound';
+const mockErrorUrl = 'glob:*/mock/error/url';
+const mock503 = 'glob:*/mock/503';
const mockGetPayload = { get: 'payload' };
const mockPostPayload = { post: 'payload' };
@@ -50,20 +53,23 @@ const mockCachePayload = {
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
describe('callApi()', () => {
- beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
+ beforeAll(() => {
+ fetchMock.mockGlobal();
+ fetchMock.get(LOGIN_GLOB, { result: '1234' });
+ });
beforeEach(() => {
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.put(mockPutUrl, mockPutPayload);
fetchMock.patch(mockPatchUrl, mockPatchPayload);
- fetchMock.get(mockCacheUrl, mockCachePayload);
+ fetchMock.get(mockCacheUrl, mockCachePayload, { name: mockCacheUrl });
fetchMock.get(mockNotFound, { status: 404 });
fetchMock.get(mock503, { status: 503 });
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
});
- afterEach(() => fetchMock.reset());
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
describe('request config', () => {
it('calls the right url with the specified method', async () => {
@@ -74,10 +80,10 @@ describe('callApi()', () => {
callApi({ url: mockPutUrl, method: 'PUT' }),
callApi({ url: mockPatchUrl, method: 'PATCH' }),
]);
- expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
- expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockPatchUrl)).toHaveLength(1);
});
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', async () => {
@@ -92,12 +98,11 @@ describe('callApi()', () => {
},
redirect: 'follow',
signal: undefined,
- body: 'BODY',
};
await callApi(mockRequest);
- const calls = fetchMock.calls(mockGetUrl);
- const fetchParams = calls[0][1] as RequestInit;
+ const calls = fetchMock.callHistory.calls(mockGetUrl);
+ const fetchParams = calls[0].options as RequestInit;
expect(calls).toHaveLength(1);
expect(fetchParams.mode).toBe(mockRequest.mode);
expect(fetchParams.cache).toBe(mockRequest.cache);
@@ -119,10 +124,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
- const calls = fetchMock.calls(mockPostUrl);
+ const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -136,10 +141,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
- const calls = fetchMock.calls(mockPostUrl);
+ const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -167,13 +172,13 @@ describe('callApi()', () => {
}),
callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }),
]);
- const calls = fetchMock.calls(mockPostUrl);
+ const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(3);
- const stringified = (calls[0][1] as RequestInit).body as FormData;
- const unstringified = (calls[1][1] as RequestInit).body as FormData;
+ const stringified = (calls[0].options as RequestInit).body as FormData;
+ const unstringified = (calls[1].options as RequestInit).body as FormData;
const jsonRequestBody = JSON.parse(
- (calls[2][1] as RequestInit).body as string,
+ (calls[2].options as RequestInit).body as string,
) as JsonObject;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -211,9 +216,9 @@ describe('callApi()', () => {
stringify: false,
});
- const calls = fetchMock.calls(mockPostUrl);
+ const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
- const unstringified = (calls[0][1] as RequestInit).body as FormData;
+ const unstringified = (calls[0].options as RequestInit).body as FormData;
const hasCorruptKey = unstringified.has('corrupt');
expect(hasCorruptKey).toBeFalsy();
// When a corrupt attribute is encountered, a console.error call is made with info about the corrupt attribute
@@ -228,10 +233,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
- const calls = fetchMock.calls(mockPutUrl);
+ const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -245,10 +250,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
- const calls = fetchMock.calls(mockPutUrl);
+ const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -275,11 +280,11 @@ describe('callApi()', () => {
stringify: false,
}),
]);
- const calls = fetchMock.calls(mockPutUrl);
+ const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(2);
- const stringified = (calls[0][1] as RequestInit).body as FormData;
- const unstringified = (calls[1][1] as RequestInit).body as FormData;
+ const stringified = (calls[0].options as RequestInit).body as FormData;
+ const unstringified = (calls[1].options as RequestInit).body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
expect(stringified.get(key)).toBe(JSON.stringify(value));
@@ -294,10 +299,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
- const calls = fetchMock.calls(mockPatchUrl);
+ const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -311,10 +316,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
- const calls = fetchMock.calls(mockPatchUrl);
+ const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(1);
- const fetchParams = calls[0][1] as RequestInit;
+ const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -341,11 +346,11 @@ describe('callApi()', () => {
stringify: false,
}),
]);
- const calls = fetchMock.calls(mockPatchUrl);
+ const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(2);
- const stringified = (calls[0][1] as RequestInit).body as FormData;
- const unstringified = (calls[1][1] as RequestInit).body as FormData;
+ const stringified = (calls[0].options as RequestInit).body as FormData;
+ const unstringified = (calls[1].options as RequestInit).body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
expect(stringified.get(key)).toBe(JSON.stringify(value));
@@ -373,7 +378,7 @@ describe('callApi()', () => {
it('caches requests with ETags', async () => {
expect.assertions(2);
await callApi({ url: mockCacheUrl, method: 'GET' });
- const calls = fetchMock.calls(mockCacheUrl);
+ const calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const supersetCache = await caches.open(constants.CACHE_KEY);
const cachedResponse = await supersetCache.match(mockCacheUrl);
@@ -385,7 +390,7 @@ describe('callApi()', () => {
window.location.protocol = 'http:';
await callApi({ url: mockCacheUrl, method: 'GET' });
- const calls = fetchMock.calls(mockCacheUrl);
+ const calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const supersetCache = await caches.open(constants.CACHE_KEY);
@@ -399,7 +404,7 @@ describe('callApi()', () => {
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
- let calls = fetchMock.calls(mockCacheUrl);
+ let calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const firstBody = await firstResponse.text();
expect(firstBody).toEqual('BODY');
@@ -408,8 +413,8 @@ describe('callApi()', () => {
url: mockCacheUrl,
method: 'GET',
});
- calls = fetchMock.calls(mockCacheUrl);
- const fetchParams = calls[1][1] as RequestInit;
+ calls = fetchMock.callHistory.calls(mockCacheUrl);
+ const fetchParams = calls[1].options as RequestInit;
expect(calls).toHaveLength(2);
// second call should not have If-None-Match header
expect(fetchParams.headers).toBeUndefined();
@@ -424,14 +429,14 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
- let calls = fetchMock.calls(mockCacheUrl);
+ let calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
// second call sends the Etag in the If-None-Match header
await callApi({ url: mockCacheUrl, method: 'GET' });
- calls = fetchMock.calls(mockCacheUrl);
- const fetchParams = calls[1][1] as RequestInit;
- const headers = { 'If-None-Match': 'etag' };
+ calls = fetchMock.callHistory.calls(mockCacheUrl);
+ const fetchParams = calls[1].options as RequestInit;
+ const headers = { 'if-none-match': 'etag' };
expect(calls).toHaveLength(2);
expect(fetchParams.headers).toEqual(
expect.objectContaining(headers) as typeof fetchParams.headers,
@@ -442,16 +447,16 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
- expect(fetchMock.calls(mockCacheUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(1);
// second call reuses the cached payload on a 304
const mockCachedPayload = { status: 304 };
- fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
+ fetchMock.modifyRoute(mockCacheUrl, { response: mockCachedPayload });
const secondResponse = await callApi({
url: mockCacheUrl,
method: 'GET',
});
- expect(fetchMock.calls(mockCacheUrl)).toHaveLength(2);
+ expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(2);
const secondBody = await secondResponse.text();
expect(secondBody).toEqual('BODY');
});
@@ -461,7 +466,7 @@ describe('callApi()', () => {
// this should never happen, since a 304 is only returned if we have
// the cached response and sent the If-None-Match header
- const mockUncachedUrl = '/mock/uncached/url';
+ const mockUncachedUrl = 'glob:*/mock/uncached/url';
const mockCachedPayload = { status: 304 };
let error;
fetchMock.get(mockUncachedUrl, mockCachedPayload);
@@ -471,7 +476,7 @@ describe('callApi()', () => {
} catch (err) {
error = err;
} finally {
- const calls = fetchMock.calls(mockUncachedUrl);
+ const calls = fetchMock.callHistory.calls(mockUncachedUrl);
expect(calls).toHaveLength(1);
expect((error as { message: string }).message).toEqual(
'Received 304 but no content is cached!',
@@ -483,7 +488,7 @@ describe('callApi()', () => {
expect.assertions(3);
const url = mockGetUrl;
const response = await callApi({ url, method: 'GET' });
- const calls = fetchMock.calls(url);
+ const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(1);
expect(response.status).toEqual(200);
const body = await response.json();
@@ -494,7 +499,7 @@ describe('callApi()', () => {
expect.assertions(2);
const url = mockNotFound;
const response = await callApi({ url, method: 'GET' });
- const calls = fetchMock.calls(url);
+ const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(1);
expect(response.status).toEqual(404);
});
@@ -513,7 +518,7 @@ describe('callApi()', () => {
error = err;
} finally {
const err = error as { status: number; statusText: string };
- expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4);
+ expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(4);
expect(err.status).toBe(mockErrorPayload.status);
expect(err.statusText).toBe(mockErrorPayload.statusText);
}
@@ -531,7 +536,7 @@ describe('callApi()', () => {
} catch (err) {
error = err as { status: number; statusText: string };
} finally {
- expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(1);
expect(error?.status).toBe(mockErrorPayload.status);
expect(error?.statusText).toBe(mockErrorPayload.statusText);
}
@@ -545,7 +550,7 @@ describe('callApi()', () => {
url,
method: 'GET',
});
- const calls = fetchMock.calls(url);
+ const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(4);
expect(response.status).toEqual(503);
});
@@ -581,7 +586,9 @@ describe('callApi()', () => {
const result = await response.json();
expect(response.status).toEqual(200);
expect(result).toEqual({ yes: 'ok' });
- expect(fetchMock.lastUrl()).toEqual(`http://localhost/get-search?abc=1`);
+ expect(fetchMock.callHistory.lastCall()?.url).toEqual(
+ `http://localhost/get-search?abc=1`,
+ );
});
it('should accept URLSearchParams', async () => {
@@ -596,8 +603,10 @@ describe('callApi()', () => {
method: 'POST',
jsonPayload: { request: 'ok' },
});
- expect(fetchMock.lastUrl()).toEqual(`http://localhost/post-search?abc=1`);
- expect(fetchMock.lastOptions()).toEqual(
+ expect(fetchMock.callHistory.lastCall()?.url).toEqual(
+ `http://localhost/post-search?abc=1`,
+ );
+ expect(fetchMock.callHistory.lastCall()?.options).toEqual(
expect.objectContaining({
body: JSON.stringify({ request: 'ok' }),
}),
@@ -634,7 +643,7 @@ describe('callApi()', () => {
method: 'POST',
postPayload: payload,
});
- expect(fetchMock.lastOptions()?.body).toBe(payload);
+ expect(fetchMock.callHistory.lastCall()?.options.body).toBe(payload);
});
it('should ignore "null" postPayload string', async () => {
@@ -646,6 +655,6 @@ describe('callApi()', () => {
method: 'POST',
postPayload: 'null',
});
- expect(fetchMock.lastOptions()?.body).toBeUndefined();
+ expect(fetchMock.callHistory.lastCall()?.options.body).toBeUndefined();
});
});
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts
index e0bf14e6c8ee..a31dd48d3489 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/callApiAndParseWithTimeout.test.ts
@@ -30,15 +30,16 @@ import { LOGIN_GLOB } from '../fixtures/constants';
const mockGetUrl = '/mock/get/url';
const mockGetPayload = { get: 'payload' };
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('callApiAndParseWithTimeout()', () => {
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
beforeEach(() => fetchMock.get(mockGetUrl, mockGetPayload));
- afterAll(() => fetchMock.restore());
-
afterEach(() => {
- fetchMock.reset();
+ fetchMock.removeRoutes().clearHistory();
jest.useRealTimers();
});
@@ -108,7 +109,7 @@ describe('callApiAndParseWithTimeout()', () => {
} catch (err) {
error = err;
} finally {
- expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockTimeoutUrl)).toHaveLength(1);
expect(error).toEqual({
error: 'Request timed out',
statusText: 'timeout',
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts
index 4b6192e65ac0..030c90f4a82a 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts
@@ -22,12 +22,15 @@ import parseResponse from '../../../src/connection/callApi/parseResponse';
import { LOGIN_GLOB } from '../fixtures/constants';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('parseResponse()', () => {
beforeAll(() => {
fetchMock.get(LOGIN_GLOB, { result: '1234' });
});
- afterAll(() => fetchMock.restore());
+ afterAll(() => fetchMock.removeRoutes().clearHistory());
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
@@ -45,7 +48,7 @@ describe('parseResponse()', () => {
fetchMock.get(mockNoParseUrl, new Response('test response'));
});
- afterEach(() => fetchMock.reset());
+ afterEach(() => fetchMock.removeRoutes().clearHistory());
it('returns a Promise', () => {
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
@@ -58,7 +61,7 @@ describe('parseResponse()', () => {
const args = await parseResponse(
callApi({ url: mockGetUrl, method: 'GET' }),
);
- expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
const keys = Object.keys(args);
expect(keys).toContain('response');
expect(keys).toContain('json');
@@ -81,7 +84,7 @@ describe('parseResponse()', () => {
} catch (err) {
error = err as Error;
} finally {
- expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(error?.stack).toBeDefined();
expect(error?.message).toContain('Unexpected token');
}
@@ -99,7 +102,7 @@ describe('parseResponse()', () => {
callApi({ url: mockTextParseUrl, method: 'GET' }),
'text',
);
- expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockTextParseUrl)).toHaveLength(1);
const keys = Object.keys(args);
expect(keys).toContain('response');
expect(keys).toContain('text');
@@ -134,7 +137,7 @@ describe('parseResponse()', () => {
callApi({ url: mockNoParseUrl, method: 'GET' }),
'raw',
);
- expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(2);
+ expect(fetchMock.callHistory.calls(mockNoParseUrl)).toHaveLength(2);
expect(responseNull.bodyUsed).toBe(false);
expect(responseRaw.bodyUsed).toBe(false);
});
@@ -193,7 +196,7 @@ describe('parseResponse()', () => {
} catch (err) {
error = err as { ok: boolean; status: number };
} finally {
- expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(mockNotOkayUrl)).toHaveLength(1);
expect(error?.ok).toBe(false);
expect(error?.status).toBe(404);
}
diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts
index c5bb3fcd83a1..e5cc8a19beb0 100644
--- a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getDatasourceMetadata.test.ts
@@ -21,10 +21,13 @@ import { getDatasourceMetadata } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('getFormData()', () => {
beforeAll(() => setupClientForTest());
- afterEach(() => fetchMock.restore());
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns datasource metadata for given datasource key', () => {
const mockData = {
diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts
index 4987d8b91d65..7976e5e8870c 100644
--- a/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/query/api/legacy/getFormData.test.ts
@@ -22,10 +22,13 @@ import { getFormData } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('getFormData()', () => {
beforeAll(() => setupClientForTest());
- afterEach(() => fetchMock.restore());
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
const mockData = {
datasource: '1__table',
diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts
index f88c44a2312f..e2c4d61ac3c3 100644
--- a/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/query/api/v1/getChartData.test.ts
@@ -20,9 +20,13 @@ import fetchMock from 'fetch-mock';
import { buildQueryContext, ApiV1, VizType } from '@superset-ui/core';
import setupClientForTest from '../setupClientForTest';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('API v1 > getChartData()', () => {
beforeAll(() => setupClientForTest());
- afterEach(() => fetchMock.restore());
+
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns a promise of ChartDataResponse', async () => {
const response = {
diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
index d7fcf1c04c85..5e39c6f8cfe2 100644
--- a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
@@ -21,9 +21,13 @@ import { JsonValue, SupersetClientClass } from '@superset-ui/core';
import { makeApi, SupersetApiError } from '../../../../src/query';
import setupClientForTest from '../setupClientForTest';
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
describe('makeApi()', () => {
beforeAll(() => setupClientForTest());
- afterEach(() => fetchMock.restore());
+
+ afterEach(() => fetchMock.clearHistory().removeRoutes());
it('should expose method and endpoint', () => {
const api = makeApi({
@@ -95,7 +99,7 @@ describe('makeApi()', () => {
const expected = new FormData();
expected.append('request', JSON.stringify('test'));
- const received = fetchMock.lastOptions()?.body as FormData;
+ const received = fetchMock.callHistory.lastCall()?.options.body as FormData;
expect(received).toBeInstanceOf(FormData);
expect(received.get('request')).toEqual(expected.get('request'));
@@ -109,7 +113,7 @@ describe('makeApi()', () => {
});
fetchMock.get('glob:*/test-get-search*', { search: 'get' });
await api({ p1: 1, p2: 2, p3: [1, 2] });
- expect(fetchMock.lastUrl()).toContain(
+ expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-get-search?p1=1&p2=2&p3=1%2C2',
);
});
@@ -123,7 +127,7 @@ describe('makeApi()', () => {
});
fetchMock.get('glob:*/test-post-search*', { rison: 'get' });
await api({ p1: 1, p3: [1, 2] });
- expect(fetchMock.lastUrl()).toContain(
+ expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-post-search?q=(p1:1,p3:!(1,2))',
);
});
@@ -137,7 +141,9 @@ describe('makeApi()', () => {
});
fetchMock.post('glob:*/test-post-search*', { search: 'post' });
await api({ p1: 1, p3: [1, 2] });
- expect(fetchMock.lastUrl()).toContain('/test-post-search?p1=1&p3=1%2C2');
+ expect(fetchMock.callHistory.lastCall()?.url).toContain(
+ '/test-post-search?p1=1&p3=1%2C2',
+ );
});
it('should throw when requestType is invalid', () => {
@@ -215,6 +221,8 @@ describe('makeApi()', () => {
fetchMock.delete('glob:*/test-raw-response?*', 'ok');
const result = await api({ field1: 11 }, {});
expect(result).toEqual(200);
- expect(fetchMock.lastUrl()).toContain('/test-raw-response?field1=11');
+ expect(fetchMock.callHistory.lastCall()?.url).toContain(
+ '/test-raw-response?field1=11',
+ );
});
});
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
index 152cdb2b684a..84bbaa5e6282 100644
--- a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
@@ -25,7 +25,10 @@ import {
formatTimeRangeComparison,
} from '../../src/time-comparison/fetchTimeRange';
-afterEach(() => fetchMock.restore());
+beforeAll(() => fetchMock.mockGlobal());
+afterAll(() => fetchMock.hardReset());
+
+afterEach(() => fetchMock.clearHistory().removeRoutes());
test('generates proper time range string', () => {
expect(
@@ -84,34 +87,41 @@ test('returns a formatted time range from empty response', async () => {
});
test('returns a formatted error message from response', async () => {
- fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
- throws: new Response(JSON.stringify({ message: 'Network error' })),
- });
+ const getTimeRangeUrl = 'glob:*/api/v1/time_range/?q=%27Last+day%27';
+ fetchMock.get(
+ getTimeRangeUrl,
+ {
+ throws: new Response(JSON.stringify({ message: 'Network error' })),
+ },
+ { name: getTimeRangeUrl },
+ );
let timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({
error: 'Network error',
});
+ fetchMock.removeRoute(getTimeRangeUrl);
fetchMock.get(
- 'glob:*/api/v1/time_range/?q=%27Last+day%27',
+ getTimeRangeUrl,
{
throws: new Error('Internal Server Error'),
},
- { overwriteRoutes: true },
+ { name: getTimeRangeUrl },
);
timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({
error: 'Internal Server Error',
});
+ fetchMock.removeRoute(getTimeRangeUrl);
fetchMock.get(
- 'glob:*/api/v1/time_range/?q=%27Last+day%27',
+ getTimeRangeUrl,
{
throws: new Response(JSON.stringify({ statusText: 'Network error' }), {
statusText: 'Network error',
}),
},
- { overwriteRoutes: true },
+ { name: getTimeRangeUrl },
);
timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({
diff --git a/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts b/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts
index 81279356c35f..1db1bafac386 100644
--- a/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts
+++ b/superset-frontend/spec/helpers/jsDomWithFetchAPI.ts
@@ -31,6 +31,7 @@ export default class FixJSDOMEnvironment extends JSDOMEnvironment {
this.global.Response = Response;
this.global.AbortSignal = AbortSignal;
this.global.AbortController = AbortController;
+ this.global.ReadableStream = ReadableStream;
// Mock MessageChannel to prevent hanging Jest tests with rc-overflow@1.4.1
// Forces rc-overflow to use requestAnimationFrame fallback instead
diff --git a/superset-frontend/spec/helpers/shim.tsx b/superset-frontend/spec/helpers/shim.tsx
index 112b0256f849..9e49116df420 100644
--- a/superset-frontend/spec/helpers/shim.tsx
+++ b/superset-frontend/spec/helpers/shim.tsx
@@ -23,6 +23,7 @@ import jQuery from 'jquery';
// https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options
// in order to mock modules in test case, so avoid absolute import module
import { configure as configureTranslation } from '@apache-superset/core/ui';
+import fetchMock from 'fetch-mock';
import { Worker } from './Worker';
import { IntersectionObserver } from './IntersectionObserver';
import { ResizeObserver } from './ResizeObserver';
@@ -43,6 +44,9 @@ if (defaultView != null) {
});
}
+fetchMock.mockGlobal();
+fetchMock.config.allowRelativeUrls = true;
+
const g = global as any;
g.window ??= Object.create(window);
g.window.location ??= { href: 'about:blank' };
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index ac6263d72dd5..ae547da87d3d 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -86,21 +86,30 @@ describe('async actions', () => {
};
let dispatch;
+ const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
+ const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
beforeEach(() => {
dispatch = sinon.spy();
- });
-
- afterEach(() => fetchMock.resetHistory());
+ fetchMock.removeRoute(fetchQueryEndpoint);
+ fetchMock.get(
+ fetchQueryEndpoint,
+ JSON.stringify({
+ data: mockBigNumber,
+ query: { sqlEditorId: 'dfsadfs' },
+ }),
+ { name: fetchQueryEndpoint },
+ );
- const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
- fetchMock.get(
- fetchQueryEndpoint,
- JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }),
- );
+ fetchMock.removeRoute(runQueryEndpoint);
+ fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`, {
+ name: runQueryEndpoint,
+ });
+ });
- const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
- fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
+ afterEach(() => {
+ fetchMock.clearHistory();
+ });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveQuery', () => {
@@ -117,15 +126,15 @@ describe('async actions', () => {
const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
- expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(saveQueryEndpoint)).toHaveLength(1);
});
});
test('posts the correct query object', () => {
const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
- const call = fetchMock.calls(saveQueryEndpoint)[0];
- const formData = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(saveQueryEndpoint)[0];
+ const formData = JSON.parse(call.options.body);
const mappedQueryToServer = actions.convertQueryToServer(query);
Object.keys(mappedQueryToServer).forEach(key => {
@@ -172,11 +181,12 @@ describe('async actions', () => {
const expectedSql = 'SELECT 1';
beforeEach(() => {
+ fetchMock.removeRoute(formatQueryEndpoint);
fetchMock.post(
formatQueryEndpoint,
{ result: expectedSql },
{
- overwriteRoutes: true,
+ name: formatQueryEndpoint,
},
);
});
@@ -185,7 +195,9 @@ describe('async actions', () => {
const store = mockStore(initialState);
store.dispatch(actions.formatQuery(query, queryId));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL);
expect(store.getActions()[0].sql).toBe(expectedSql);
@@ -209,11 +221,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithoutExtras));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
- const call = fetchMock.calls(formatQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body).toEqual({ sql: 'SELECT * FROM table' });
expect(body.database_id).toBeUndefined();
@@ -238,11 +252,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithDb));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
- const call = fetchMock.calls(formatQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table',
@@ -268,11 +284,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithTemplateString));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
- const call = fetchMock.calls(formatQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
@@ -299,11 +317,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithTemplateObject));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
- const call = fetchMock.calls(formatQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
@@ -314,12 +334,11 @@ describe('async actions', () => {
test('dispatches QUERY_EDITOR_SET_SQL with formatted result', async () => {
const formattedSql = 'SELECT\n *\nFROM\n table';
- fetchMock.post(
+ fetchMock.removeRoute(formatQueryEndpoint);
+ fetchMock.route(
formatQueryEndpoint,
{ result: formattedSql },
- {
- overwriteRoutes: true,
- },
+ { name: formatQueryEndpoint },
);
const queryEditorToFormat = {
@@ -365,11 +384,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(outdatedQueryEditor));
await waitFor(() =>
- expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
+ 1,
+ ),
);
- const call = fetchMock.calls(formatQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body.sql).toBe('SELECT * FROM updated_table');
expect(body.database_id).toBe(10);
@@ -388,7 +409,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
- expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1);
});
});
@@ -402,7 +423,7 @@ describe('async actions', () => {
test.skip('parses large number result without losing precision', () =>
makeRequest().then(() => {
- expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
mockBigNumber,
@@ -427,10 +448,11 @@ describe('async actions', () => {
test('calls queryFailed on fetch error', () => {
expect.assertions(1);
+ fetchMock.removeRoute(fetchQueryEndpoint);
fetchMock.get(
fetchQueryEndpoint,
{ throws: { message: 'error text' } },
- { overwriteRoutes: true },
+ { name: fetchQueryEndpoint },
);
const store = mockStore({});
@@ -457,7 +479,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
- expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
});
});
@@ -469,9 +491,9 @@ describe('async actions', () => {
});
});
- test.skip('parses large number result without losing precision', () =>
+ test('parses large number result without losing precision', () =>
makeRequest().then(() => {
- expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
mockBigNumber,
@@ -495,6 +517,7 @@ describe('async actions', () => {
test('calls queryFailed on fetch error and logs the error details', () => {
expect.assertions(2);
+ fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(
runQueryEndpoint,
{
@@ -504,7 +527,7 @@ describe('async actions', () => {
statusText: 'timeout',
},
},
- { overwriteRoutes: true },
+ { name: runQueryEndpoint },
);
const store = mockStore({});
@@ -550,7 +573,9 @@ describe('async actions', () => {
`{ "data": ${mockBigNumber} }`,
);
await makeRequest().then(() => {
- expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1);
+ expect(
+ fetchMock.callHistory.calls(runQueryEndpointWithParams),
+ ).toHaveLength(1);
});
});
});
@@ -591,7 +616,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
- expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(stopQueryEndpoint)).toHaveLength(1);
});
});
@@ -607,8 +632,8 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
- const call = fetchMock.calls(stopQueryEndpoint)[0];
- const body = JSON.parse(call[1].body);
+ const call = fetchMock.callHistory.calls(stopQueryEndpoint)[0];
+ const body = JSON.parse(call.options.body);
expect(body.client_id).toBe(baseQuery.id);
});
});
@@ -955,7 +980,7 @@ describe('async actions', () => {
isFeatureEnabled.mockRestore();
});
- afterEach(() => fetchMock.resetHistory());
+ afterEach(() => fetchMock.clearHistory());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
@@ -978,7 +1003,9 @@ describe('async actions', () => {
store.dispatch(actions.addQueryEditor(queryEditor));
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(0);
});
});
@@ -1121,7 +1148,9 @@ describe('async actions', () => {
const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
return request(store.dispatch, store.getState).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(1);
});
});
});
@@ -1143,7 +1172,9 @@ describe('async actions', () => {
request(store.dispatch, store.getState);
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(0);
isFeatureEnabled.mockRestore();
});
});
@@ -1325,10 +1356,14 @@ describe('async actions', () => {
expectedActionTypes,
);
expect(store.getActions()[0].prepend).toBeFalsy();
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ expect(
+ fetchMock.callHistory.calls(updateTableSchemaEndpoint),
+ ).toHaveLength(1);
// tab state is not updated, since no query was run
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(0);
});
});
});
@@ -1354,14 +1389,15 @@ describe('async actions', () => {
});
beforeEach(() => {
+ fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
- overwriteRoutes: true,
+ name: runQueryEndpoint,
});
});
afterEach(() => {
store.clearActions();
- fetchMock.resetHistory();
+ fetchMock.clearHistory();
});
test('updates and runs data preview query when configured', () => {
@@ -1382,9 +1418,11 @@ describe('async actions', () => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
- expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
// tab state is not updated, since the query is a data preview
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(0);
});
});
@@ -1406,9 +1444,11 @@ describe('async actions', () => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
- expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
// tab state is not updated, since the query is a data preview
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(0);
});
});
});
@@ -1428,13 +1468,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
@@ -1454,7 +1494,7 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
@@ -1480,13 +1520,13 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1510,13 +1550,13 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
isFeatureEnabled.mockRestore();
@@ -1539,13 +1579,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
@@ -1564,13 +1604,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1589,13 +1629,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
- call[0] &&
- call[0].includes('/tableschemaview/') &&
- call[0].includes('/expanded'),
+ call.url &&
+ call.url.includes('/tableschemaview/') &&
+ call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1618,7 +1658,7 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- const expandedCalls = fetchMock
+ const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
@@ -1647,7 +1687,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables([table])).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ expect(
+ fetchMock.callHistory.calls(updateTableSchemaEndpoint),
+ ).toHaveLength(1);
});
});
@@ -1667,7 +1709,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables(tables)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
+ expect(
+ fetchMock.callHistory.calls(updateTableSchemaEndpoint),
+ ).toHaveLength(2);
});
});
@@ -1684,7 +1728,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables(tables)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ expect(
+ fetchMock.callHistory.calls(updateTableSchemaEndpoint),
+ ).toHaveLength(1);
});
});
});
@@ -1699,8 +1745,9 @@ describe('async actions', () => {
query: { sqlEditorId: 'null' },
query_id: 'efgh',
};
+ fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
- overwriteRoutes: true,
+ name: runQueryEndpoint,
});
const oldQueryEditor = { ...queryEditor, inLocalStorage: true };
@@ -1777,10 +1824,14 @@ describe('async actions', () => {
.dispatch(actions.syncQueryEditor(oldQueryEditor))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3);
+ expect(
+ fetchMock.callHistory.calls(updateTabStateEndpoint),
+ ).toHaveLength(3);
// query editor has 2 tables loaded in the schema viewer
- expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
+ expect(
+ fetchMock.callHistory.calls(updateTableSchemaEndpoint),
+ ).toHaveLength(2);
});
});
});
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
index bb86e631eb04..1515b9a1694a 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
@@ -53,7 +53,11 @@ const StyledContainer = styled.div`
const StyledSidebar = styled.div`
position: relative;
- padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
+ padding: ${({ theme }) => theme.sizeUnit * 2.5}px 0;
+ margin: 0 ${({ theme }) => theme.sizeUnit * 2.5}px;
+ flex: 1;
+ height: 100%;
+ background-color: ${({ theme }) => theme.colorBgBase};
`;
const ContentWrapper = styled.div`
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
index 2116340b7754..0870724176b5 100644
--- a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
@@ -74,13 +74,13 @@ beforeEach(() => {
});
afterEach(() => {
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
});
test('sync the unsaved editor tab state when there are new changes since the last update', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
- expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(, {
useRedux: true,
initialState: {
@@ -91,14 +91,14 @@ test('sync the unsaved editor tab state when there are new changes since the las
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
- fetchMock.restore();
+ expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(1);
+ fetchMock.clearHistory().removeRoutes();
});
test('sync the unsaved NEW editor state when there are new in local storage', async () => {
const createEditorTabState = `glob:*/tabstateview/`;
fetchMock.post(createEditorTabState, { id: 123 });
- expect(fetchMock.calls(createEditorTabState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(0);
render(, {
useRedux: true,
initialState: {
@@ -119,12 +119,14 @@ test('sync the unsaved NEW editor state when there are new in local storage', as
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(createEditorTabState)).toHaveLength(1);
- fetchMock.restore();
+ expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(1);
+ fetchMock.clearHistory().removeRoutes();
});
test('sync the active editor id when there are updates in tab history', async () => {
- expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
+ 0,
+ );
render(, {
useRedux: true,
initialState: {
@@ -147,18 +149,22 @@ test('sync the active editor id when there are updates in tab history', async ()
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
+ 1,
+ );
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
+ 1,
+ );
});
test('sync the destroyed editor id when there are updates in destroyed editors', async () => {
const removeId = 'removed-tab-id';
const deleteEditorState = `glob:*/tabstateview/${removeId}`;
fetchMock.delete(deleteEditorState, { id: removeId });
- expect(fetchMock.calls(deleteEditorState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(0);
render(, {
useRedux: true,
initialState: {
@@ -174,17 +180,17 @@ test('sync the destroyed editor id when there are updates in destroyed editors',
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1);
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1);
});
test('skip syncing the unsaved editor tab state when the updates are already synced', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
- expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(, {
useRedux: true,
initialState: {
@@ -203,8 +209,8 @@ test('skip syncing the unsaved editor tab state when the updates are already syn
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
- expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
- fetchMock.restore();
+ expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
+ fetchMock.clearHistory().removeRoutes();
});
test('renders an error toast when the sync failed', async () => {
@@ -212,7 +218,7 @@ test('renders an error toast when the sync failed', async () => {
fetchMock.put(updateEditorTabState, {
throws: new Error('errorMessage'),
});
- expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(
<>
@@ -235,5 +241,5 @@ test('renders an error toast when the sync failed', async () => {
'An error occurred while saving your editor state.',
expect.anything(),
);
- fetchMock.restore();
+ fetchMock.clearHistory().removeRoutes();
});
diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts
index 99377c43be07..4fcf7132b3c9 100644
--- a/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts
+++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts
@@ -50,14 +50,16 @@ jest.mock('@superset-ui/core', () => ({
}));
afterEach(() => {
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
act(() => {
store.dispatch(api.util.resetApiState());
});
});
beforeEach(() => {
- fetchMock.post(queryValidationApiRoute, fakeApiResult);
+ fetchMock.post(queryValidationApiRoute, fakeApiResult, {
+ name: queryValidationApiRoute,
+ });
});
const initialize = (withValidator = false) => {
@@ -115,13 +117,15 @@ const initialize = (withValidator = false) => {
test('skips fetching validation if validator is undefined', () => {
const { result } = initialize();
expect(result.current.data).toEqual([]);
- expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(0);
});
test('returns validation if validator is configured', async () => {
const { result, waitFor } = initialize(true);
await waitFor(() =>
- expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(
+ 1,
+ ),
);
expect(result.current.data).toEqual(
fakeApiResult.result.map(err => ({
@@ -135,13 +139,10 @@ test('returns validation if validator is configured', async () => {
test('returns server error description', async () => {
const errorMessage = 'Unexpected validation api error';
- fetchMock.post(
- queryValidationApiRoute,
- {
- throws: new Error(errorMessage),
- },
- { overwriteRoutes: true },
- );
+ fetchMock.removeRoute(queryValidationApiRoute);
+ fetchMock.post(queryValidationApiRoute, {
+ throws: new Error(errorMessage),
+ });
const { result, waitFor } = initialize(true);
await waitFor(
() =>
@@ -159,13 +160,10 @@ test('returns server error description', async () => {
test('returns session expire description when CSRF token expired', async () => {
const errorMessage = 'CSRF token expired';
- fetchMock.post(
- queryValidationApiRoute,
- {
- throws: new Error(errorMessage),
- },
- { overwriteRoutes: true },
- );
+ fetchMock.removeRoute(queryValidationApiRoute);
+ fetchMock.post(queryValidationApiRoute, {
+ throws: new Error(errorMessage),
+ });
const { result, waitFor } = initialize(true);
await waitFor(
() =>
diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts
index 3e4a75646733..e54322cca182 100644
--- a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts
+++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts
@@ -94,7 +94,7 @@ beforeEach(() => {
});
afterEach(() => {
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
act(() => {
store.dispatch(api.util.resetApiState());
});
@@ -120,7 +120,7 @@ test('returns keywords including fetched function_names data', async () => {
);
await waitFor(() =>
- expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1),
+ expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1),
);
fakeSchemaApiResult.forEach(schema => {
expect(result.current).toContainEqual(
@@ -171,7 +171,7 @@ test('skip fetching if autocomplete skipped', () => {
},
);
expect(result.current).toEqual([]);
- expect(fetchMock.calls()).toEqual([]);
+ expect(fetchMock.callHistory.calls()).toEqual([]);
});
test('returns column keywords among selected tables', async () => {
diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx
index 16cb8374ce70..bcd7e3aa02f7 100644
--- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/ExploreCtasResultsButton.test.tsx
@@ -62,7 +62,7 @@ describe('ExploreCtasResultsButton', () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
fetchMock.post(getOrCreateTableEndpoint, { result: { table_id: 1234 } });
fireEvent.click(getByText('Explore'));
@@ -80,7 +80,7 @@ describe('ExploreCtasResultsButton', () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
fetchMock.post(getOrCreateTableEndpoint, {
throws: new Error('Unexpected all to v1 API'),
});
diff --git a/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx
index e66e5bba91c4..1e94927fef54 100644
--- a/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx
+++ b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx
@@ -57,7 +57,7 @@ beforeEach(() => {
});
afterEach(() => {
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
});
let replaceState = jest.spyOn(window.history, 'replaceState');
@@ -78,7 +78,7 @@ test('should handle id', async () => {
setup('/sqllab?id=1');
await waitFor(() =>
expect(
- fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
+ fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
@@ -86,7 +86,7 @@ test('should handle id', async () => {
expect.anything(),
'/sqllab',
);
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
});
test('should handle permalink', async () => {
const key = '9sadkfl';
@@ -98,7 +98,7 @@ test('should handle permalink', async () => {
setup('/sqllab/p/9sadkfl');
await waitFor(() =>
expect(
- fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
+ fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
@@ -106,12 +106,14 @@ test('should handle permalink', async () => {
expect.anything(),
'/sqllab',
);
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
});
test('should handle savedQueryId', async () => {
setup('/sqllab?savedQueryId=1');
await waitFor(() =>
- expect(fetchMock.calls('glob:*/api/v1/saved_query/1')).toHaveLength(1),
+ expect(
+ fetchMock.callHistory.calls('glob:*/api/v1/saved_query/1'),
+ ).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
expect.anything(),
diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
index edce1f2ce073..d04361153257 100644
--- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
@@ -57,7 +57,7 @@ describe('QueryAutoRefresh', () => {
});
afterEach(() => {
- fetchMock.reset();
+ fetchMock.clearHistory().removeRoutes();
cleanup();
jest.runOnlyPendingTimers();
jest.useRealTimers();
@@ -162,7 +162,7 @@ describe('QueryAutoRefresh', () => {
expect(
store.getActions().filter(({ type }) => type === REFRESH_QUERIES),
).toHaveLength(0);
- expect(fetchMock.calls(refreshApi)).toHaveLength(1);
+ expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(1);
});
test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
@@ -217,7 +217,7 @@ describe('QueryAutoRefresh', () => {
),
);
- expect(fetchMock.calls(refreshApi)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(0);
});
test('logs the failed error for async queries', async () => {
diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
index 52edf19aaf74..a1d2185f3c55 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
@@ -81,7 +81,7 @@ const setup = (overrides = {}) => (
);
-afterEach(() => fetchMock.reset());
+afterEach(() => fetchMock.clearHistory().removeRoutes());
test('Renders an empty state for query history', () => {
render(setup(), { useRedux: true, initialState });
@@ -102,7 +102,7 @@ test('fetches the query history when the persistence mode is enabled', async ()
fetchMock.get(editorQueryApiRoute, fakeApiResult);
render(setup(), { useRedux: true, initialState });
await waitFor(() =>
- expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
+ expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
const queryResultText = screen.getByText(fakeApiResult.result[0].rows);
expect(queryResultText).toBeInTheDocument();
@@ -127,7 +127,7 @@ test('fetches the query history by the tabViewId', async () => {
},
});
await waitFor(() =>
- expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
+ expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
const queryResultText = screen.getByText(fakeApiResult.result[0].rows);
expect(queryResultText).toBeInTheDocument();
@@ -213,7 +213,7 @@ test('displays multiple queries with newest query first', async () => {
const { container } = render(setup(), { useRedux: true, initialState });
await waitFor(() =>
- expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
+ expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
expect(screen.getByTestId('listview-table')).toBeVisible();
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
index e12627777f47..be5a1e22fc37 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -127,7 +127,7 @@ fetchMock.post(reRunQueryEndpoint, { result: [] });
fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] });
beforeEach(() => {
- fetchMock.resetHistory();
+ fetchMock.clearHistory();
});
const middlewares = [thunk];
@@ -151,7 +151,7 @@ describe('ResultSet', () => {
// Add cleanup after each test
afterEach(async () => {
- fetchMock.resetHistory();
+ fetchMock.clearHistory();
// Wait for any pending effects to complete
await new Promise(resolve => setTimeout(resolve, 0));
});
@@ -250,7 +250,7 @@ describe('ResultSet', () => {
},
});
- expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0);
setup(mockedProps, store);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.errorMessage).toEqual(
@@ -258,7 +258,7 @@ describe('ResultSet', () => {
);
expect(store.getActions()[0].type).toEqual('START_QUERY');
await waitFor(() =>
- expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(1),
);
});
@@ -276,7 +276,7 @@ describe('ResultSet', () => {
});
setup(mockedProps, store);
expect(store.getActions()).toEqual([]);
- expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
+ expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0);
});
test('should render cached query', async () => {
@@ -622,7 +622,9 @@ describe('ResultSet', () => {
});
// Verify the API was called
- const resultsCalls = fetchMock.calls('glob:*/api/v1/sqllab/results/*');
+ const resultsCalls = fetchMock.callHistory.calls(
+ 'glob:*/api/v1/sqllab/results/*',
+ );
expect(resultsCalls).toHaveLength(1);
});
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index 6d46c4cd78aa..fa09eddb8ade 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -63,6 +63,7 @@ export type QueryPayload = {
const Styles = styled.span`
display: contents;
+ white-space: nowrap;
span[role='img']:not([aria-label='down']) {
display: flex;
margin: 0;
diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx
index cd83a05eafcb..33b408eeeddd 100644
--- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx
@@ -82,18 +82,17 @@ describe('ShareSqlLabQuery', () => {
const storeQueryMockId = 'ci39c3';
beforeEach(async () => {
+ fetchMock.removeRoute(storeQueryUrl);
fetchMock.post(
storeQueryUrl,
() => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }),
- {
- overwriteRoutes: true,
- },
+ { name: storeQueryUrl },
);
- fetchMock.resetHistory();
+ fetchMock.clearHistory();
jest.clearAllMocks();
});
- afterAll(() => fetchMock.reset());
+ afterAll(() => fetchMock.hardReset());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('via permalink api', () => {
@@ -116,10 +115,12 @@ describe('ShareSqlLabQuery', () => {
const expected = omit(mockQueryEditor, ['id', 'remoteId']);
userEvent.click(button);
await waitFor(() =>
- expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
- JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
+ JSON.parse(
+ fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string,
+ ),
).toEqual(expected);
});
@@ -140,10 +141,12 @@ describe('ShareSqlLabQuery', () => {
const expected = omit(unsavedQueryEditor, ['id']);
userEvent.click(button);
await waitFor(() =>
- expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
+ expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
- JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
+ JSON.parse(
+ fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string,
+ ),
).toEqual(expected);
});
});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
index 6eab25215f3b..86c4a6b23da8 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
@@ -75,6 +75,15 @@ jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
}));
jest.mock('src/SqlLab/components/ResultSet', () => jest.fn());
+jest.mock('src/components/DatabaseSelector', () => ({
+ __esModule: true,
+ DatabaseSelector: ({ sqlLabMode }: { sqlLabMode?: boolean }) => (
+
+ Mock DatabaseSelector
+
+ ),
+}));
+
fetchMock.get('glob:*/api/v1/database/*/function_names/', {
function_names: [],
});
@@ -389,8 +398,8 @@ describe('SqlEditor', () => {
// click button
fireEvent.click(button);
await waitFor(() => {
- expect(fetchMock.lastUrl()).toEqual(estimateApi);
- expect(fetchMock.lastOptions()).toEqual(
+ expect(fetchMock.callHistory.lastCall()?.url).toEqual(estimateApi);
+ expect(fetchMock.callHistory.lastCall()?.options).toEqual(
expect.objectContaining({
body: JSON.stringify({
database_id: 2023,
@@ -402,11 +411,11 @@ describe('SqlEditor', () => {
cache: 'default',
credentials: 'same-origin',
headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '1234',
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ 'x-csrftoken': '1234',
},
- method: 'POST',
+ method: 'post',
mode: 'same-origin',
redirect: 'follow',
signal: undefined,
@@ -443,10 +452,12 @@ describe('SqlEditor', () => {
const indicator = getByTestId('sqlEditor-loading');
expect(indicator).toBeInTheDocument();
await waitFor(() =>
- expect(fetchMock.calls('glob:*/tabstateview/*').length).toBe(1),
+ expect(
+ fetchMock.callHistory.calls('glob:*/tabstateview/*').length,
+ ).toBe(1),
);
// it will be called from EditorAutoSync
- expect(fetchMock.calls(switchTabApi).length).toBe(0);
+ expect(fetchMock.callHistory.calls(switchTabApi).length).toBe(0);
});
});
});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
index dee5e69d1d6e..673171787aeb 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
@@ -22,7 +22,6 @@ import {
screen,
userEvent,
waitFor,
- within,
} from 'spec/helpers/testing-library';
import SqlEditorLeftBar, {
SqlEditorLeftBarProps,
@@ -31,12 +30,31 @@ import {
table,
initialState,
defaultQueryEditor,
- extraQueryEditor1,
extraQueryEditor2,
} from 'src/SqlLab/fixtures';
import type { RootState } from 'src/views/store';
import type { Store } from 'redux';
+// Mock TableExploreTree to avoid complex tree rendering in tests
+jest.mock('../TableExploreTree', () => ({
+ __esModule: true,
+ default: () => (
+ TableExploreTree
+ ),
+}));
+
+// Helper to switch from default TreeView to SelectView
+const switchToSelectView = async () => {
+ const changeButton = screen.getByTestId('DatabaseSelector');
+ // Click Change button to open database selector modal
+ await userEvent.click(changeButton);
+
+ // Verify popup is opened
+ await waitFor(() => {
+ expect(screen.getByText('Select Database and Schema')).toBeInTheDocument();
+ });
+};
+
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
height: 0,
@@ -92,7 +110,7 @@ beforeEach(() => {
});
afterEach(() => {
- fetchMock.restore();
+ fetchMock.clearHistory().removeRoutes();
jest.clearAllMocks();
});
@@ -109,118 +127,16 @@ const renderAndWait = (
}),
);
-test('renders a TableElement', async () => {
- const { findByText, getAllByTestId } = await renderAndWait(
- mockedProps,
- undefined,
- {
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- tables: [table],
- databases: { [mockData.database.id]: { ...mockData.database } },
- },
- },
- );
- expect(await findByText(/Database/i)).toBeInTheDocument();
- const tableElement = getAllByTestId('table-element');
- expect(tableElement.length).toBeGreaterThanOrEqual(1);
-});
-
-test('table should be visible when expanded is true', async () => {
- const { container, getByText, getByRole, getAllByLabelText } =
- await renderAndWait(mockedProps, undefined, {
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- tables: [table],
- databases: { [mockData.database.id]: { ...mockData.database } },
- },
- });
-
- const dbSelect = getByRole('combobox', {
- name: 'Select database or type to search databases',
- });
- const schemaSelect = getByRole('combobox', {
- name: 'Select schema or type to search schemas: main',
- });
- const tableSelect = getAllByLabelText(
- /Select table or type to search tables/i,
- )[0];
- const tableOption = within(tableSelect).getByText(/ab_user/i);
-
- expect(getByText(/Database/i)).toBeInTheDocument();
- expect(dbSelect).toBeInTheDocument();
- expect(schemaSelect).toBeInTheDocument();
- expect(tableSelect).toBeInTheDocument();
- expect(tableOption).toBeInTheDocument();
- expect(
- container.querySelector('.ant-collapse-content-active'),
- ).toBeInTheDocument();
- table.columns.forEach(({ name }) => {
- expect(getByText(name)).toBeInTheDocument();
- });
-});
-
test('catalog selector should be visible when enabled in the database', async () => {
- const { container, getByText, getByRole } = await renderAndWait(
- mockedProps,
- undefined,
- {
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: mockedProps.queryEditorId,
- dbId: mockData.database.id,
- },
- tables: [table],
- databases: {
- [mockData.database.id]: {
- ...mockData.database,
- allow_multi_catalog: true,
- },
- },
- },
- },
- );
-
- const dbSelect = getByRole('combobox', {
- name: 'Select database or type to search databases',
- });
- const catalogSelect = getByRole('combobox', {
- name: 'Select catalog or type to search catalogs',
- });
- const schemaSelect = getByRole('combobox', {
- name: 'Select schema or type to search schemas',
- });
- const dropdown = getByText(/Select table/i);
- const abUser = getByText(/ab_user/i);
-
- expect(getByText(/Database/i)).toBeInTheDocument();
- expect(dbSelect).toBeInTheDocument();
- expect(catalogSelect).toBeInTheDocument();
- expect(schemaSelect).toBeInTheDocument();
- expect(dropdown).toBeInTheDocument();
- expect(abUser).toBeInTheDocument();
- expect(
- container.querySelector('.ant-collapse-content-active'),
- ).toBeInTheDocument();
- table.columns.forEach(({ name }) => {
- expect(getByText(name)).toBeInTheDocument();
- });
-});
-
-test('should toggle the table when the header is clicked', async () => {
- const { container } = await renderAndWait(mockedProps, undefined, {
+ const { getByRole } = await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
- tables: [table],
unsavedQueryEditor: {
id: mockedProps.queryEditorId,
dbId: mockData.database.id,
},
+ tables: [table],
databases: {
[mockData.database.id]: {
...mockData.database,
@@ -229,93 +145,17 @@ test('should toggle the table when the header is clicked', async () => {
},
},
});
+ await switchToSelectView();
- const header = container.querySelector('.ant-collapse-header');
- expect(header).toBeInTheDocument();
-
- if (header) {
- userEvent.click(header);
- }
-
- await waitFor(() =>
- expect(
- container.querySelector('.ant-collapse-content-inactive'),
- ).toBeInTheDocument(),
- );
-});
-
-test('When changing database the schema and table list must be updated', async () => {
- const reduxState = {
- ...initialState,
- sqlLab: {
- ...initialState.sqlLab,
- unsavedQueryEditor: {
- id: defaultQueryEditor.id,
- schema: 'db1_schema',
- dbId: mockData.database.id,
- },
- queryEditors: [
- defaultQueryEditor,
- {
- ...extraQueryEditor1,
- schema: 'new_schema',
- dbId: 2,
- },
- ],
- tables: [
- {
- ...table,
- dbId: defaultQueryEditor.dbId,
- schema: 'db1_schema',
- },
- {
- ...table,
- dbId: 2,
- schema: 'new_schema',
- name: 'new_table',
- queryEditorId: extraQueryEditor1.id,
- },
- ],
- databases: {
- [mockData.database.id]: {
- ...mockData.database,
- allow_multi_catalog: true,
- },
- 2: {
- id: 2,
- database_name: 'new_db',
- backend: 'postgresql',
- },
- },
- },
- };
- const { rerender } = await renderAndWait(mockedProps, undefined, reduxState);
-
- expect(screen.getAllByText(/main/i)[0]).toBeInTheDocument();
- expect(screen.getAllByText(/ab_user/i)[0]).toBeInTheDocument();
-
- rerender(
- ,
- );
- const updatedDbSelector = await screen.findAllByText(/new_db/i);
- expect(updatedDbSelector[0]).toBeInTheDocument();
-
- const select = screen.getByRole('combobox', {
- name: 'Select schema or type to search schemas',
+ const dbSelect = getByRole('combobox', {
+ name: 'Select database or type to search databases',
+ });
+ const catalogSelect = getByRole('combobox', {
+ name: 'Select catalog or type to search catalogs',
});
- userEvent.click(select);
-
- expect(
- await screen.findByRole('option', { name: 'main' }),
- ).toBeInTheDocument();
- expect(
- await screen.findByRole('option', { name: 'new_schema' }),
- ).toBeInTheDocument();
-
- userEvent.click(screen.getByText('new_schema'));
- const updatedTableSelector = await screen.findAllByText(/new_table/i);
- expect(updatedTableSelector[0]).toBeInTheDocument();
+ expect(dbSelect).toBeInTheDocument();
+ expect(catalogSelect).toBeInTheDocument();
});
test('display no compatible schema found when schema api throws errors', async () => {
@@ -351,10 +191,12 @@ test('display no compatible schema found when schema api throws errors', async (
undefined,
reduxState,
);
+ await switchToSelectView();
+
await waitFor(() =>
- expect(fetchMock.calls('glob:*/api/v1/database/3/schemas/?*')).toHaveLength(
- 1,
- ),
+ expect(
+ fetchMock.callHistory.calls('glob:*/api/v1/database/3/schemas/?*').length,
+ ).toBeGreaterThanOrEqual(1),
);
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
@@ -384,17 +226,12 @@ test('ignore schema api when current schema is deprecated', async () => {
},
},
});
-
- expect(await screen.findByText(/Database/i)).toBeInTheDocument();
- expect(fetchMock.calls()).not.toContainEqual(
+ await switchToSelectView();
+ expect(fetchMock.callHistory.calls()).not.toContainEqual(
expect.arrayContaining([
expect.stringContaining(
`/tables/${mockData.database.id}/${invalidSchemaName}/`,
),
]),
);
- // Deselect the deprecated schema selection
- await waitFor(() =>
- expect(screen.queryByText(/None/i)).not.toBeInTheDocument(),
- );
});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 5010298da0a2..91c8fe4e7e99 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -16,36 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useCallback, useMemo, useState } from 'react';
-import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+import { useCallback, useState } from 'react';
+import { useDispatch } from 'react-redux';
-import { SqlLabRootState, Table } from 'src/SqlLab/types';
+import { resetState } from 'src/SqlLab/actions/sqlLab';
import {
- addTable,
- removeTables,
- collapseTable,
- expandTable,
- resetState,
-} from 'src/SqlLab/actions/sqlLab';
-import { Button, EmptyState, Icons } from '@superset-ui/core/components';
+ Button,
+ EmptyState,
+ Flex,
+ Icons,
+ Popover,
+ Typography,
+} from '@superset-ui/core/components';
import { t } from '@apache-superset/core';
import { styled, css } from '@apache-superset/core/ui';
-import { TableSelectorMultiple } from 'src/components/TableSelector';
-import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
-import { noop } from 'lodash';
-import TableElement from '../TableElement';
+import type { SchemaOption, CatalogOption } from 'src/hooks/apiResources';
+import { DatabaseSelector, type DatabaseObject } from 'src/components';
+
import useDatabaseSelector from '../SqlEditorTopBar/useDatabaseSelector';
+import TableExploreTree from '../TableExploreTree';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
}
-const StyledScrollbarContainer = styled.div`
- flex: 1 1 auto;
- overflow: auto;
-`;
-
const LeftBarStyles = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.sizeUnit * 2}px;
+
${({ theme }) => css`
height: 100%;
display: flex;
@@ -53,117 +52,153 @@ const LeftBarStyles = styled.div`
.divider {
border-bottom: 1px solid ${theme.colorSplit};
- margin: ${theme.sizeUnit * 4}px 0;
+ margin: ${theme.sizeUnit * 1}px 0;
}
`}
`;
+const StyledDivider = styled.div`
+ border-bottom: 1px solid ${({ theme }) => theme.colorSplit};
+ margin: 0 -${({ theme }) => theme.sizeUnit * 2.5}px 0;
+`;
+
const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
- const { db: userSelectedDb, ...dbSelectorProps } =
- useDatabaseSelector(queryEditorId);
- const allSelectedTables = useSelector(
- ({ sqlLab }) =>
- sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
- shallowEqual,
- );
+ const dbSelectorProps = useDatabaseSelector(queryEditorId);
+ const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
+ dbSelectorProps;
+
const dispatch = useDispatch();
- const queryEditor = useQueryEditor(queryEditorId, [
- 'dbId',
- 'catalog',
- 'schema',
- 'tabViewId',
- ]);
- const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
- const { dbId, schema } = queryEditor;
- const tables = useMemo(
- () =>
- allSelectedTables.filter(
- table => table.dbId === dbId && table.schema === schema,
- ),
- [allSelectedTables, dbId, schema],
- );
+ const shouldShowReset = window.location.search === '?reset=1';
- noop(_emptyResultsWithSearch); // This is to avoid unused variable warning, can be removed if not needed
+ // Modal state for Database/Catalog/Schema selector
+ const [selectorModalOpen, setSelectorModalOpen] = useState(false);
+ const [modalDb, setModalDb] = useState(undefined);
+ const [modalCatalog, setModalCatalog] = useState<
+ CatalogOption | null | undefined
+ >(undefined);
+ const [modalSchema, setModalSchema] = useState(
+ undefined,
+ );
- const onEmptyResults = useCallback((searchText?: string) => {
- setEmptyResultsWithSearch(!!searchText);
+ const openSelectorModal = useCallback(() => {
+ setModalDb(db ?? undefined);
+ setModalCatalog(
+ catalog ? { label: catalog, value: catalog, title: catalog } : undefined,
+ );
+ setModalSchema(
+ schema ? { label: schema, value: schema, title: schema } : undefined,
+ );
+ setSelectorModalOpen(true);
+ }, [db, catalog, schema]);
+
+ const closeSelectorModal = useCallback(() => {
+ setSelectorModalOpen(false);
}, []);
- const selectedTableNames = useMemo(
- () => tables?.map(table => table.name) || [],
- [tables],
- );
-
- const onTablesChange = (
- tableNames: string[],
- catalogName: string | null,
- schemaName: string,
- ) => {
- if (!schemaName) {
- return;
+ const handleModalOk = useCallback(() => {
+ if (modalDb && modalDb.id !== db?.id) {
+ onDbChange?.(modalDb);
}
-
- const currentTables = [...tables];
- const tablesToAdd = tableNames.filter(name => {
- const index = currentTables.findIndex(table => table.name === name);
- if (index >= 0) {
- currentTables.splice(index, 1);
- return false;
- }
-
- return true;
- });
-
- tablesToAdd.forEach(tableName => {
- dispatch(addTable(queryEditor, tableName, catalogName, schemaName));
- });
-
- dispatch(removeTables(currentTables));
- };
-
- const onToggleTable = (updatedTables: string[]) => {
- tables.forEach(table => {
- if (!updatedTables.includes(table.id.toString()) && table.expanded) {
- dispatch(collapseTable(table));
- } else if (
- updatedTables.includes(table.id.toString()) &&
- !table.expanded
- ) {
- dispatch(expandTable(table));
- }
- });
- };
-
- const shouldShowReset = window.location.search === '?reset=1';
+ if (modalCatalog?.value !== catalog) {
+ onCatalogChange?.(modalCatalog?.value);
+ }
+ if (modalSchema?.value !== schema) {
+ onSchemaChange?.(modalSchema?.value ?? '');
+ }
+ setSelectorModalOpen(false);
+ }, [
+ modalDb,
+ modalCatalog,
+ modalSchema,
+ db,
+ catalog,
+ schema,
+ onDbChange,
+ onCatalogChange,
+ onSchemaChange,
+ ]);
const handleResetState = useCallback(() => {
dispatch(resetState());
}, [dispatch]);
- return (
-
-
+
+ {t('Select Database and Schema')}
+
+ }
- database={userSelectedDb}
- onTableSelectChange={onTablesChange}
- tableValue={selectedTableNames}
- sqlLabMode
+ getDbList={dbSelectorProps.getDbList}
+ handleError={dbSelectorProps.handleError}
+ onDbChange={setModalDb}
+ onCatalogChange={cat =>
+ setModalCatalog(
+ cat ? { label: cat, value: cat, title: cat } : undefined,
+ )
+ }
+ catalog={modalCatalog?.value}
+ onSchemaChange={sch =>
+ setModalSchema(
+ sch ? { label: sch, value: sch, title: sch } : undefined,
+ )
+ }
+ schema={modalSchema?.value}
+ sqlLabMode={false}
/>
-
-
- {tables.map(table => (
- expanded)
- .map(({ id }) => id)}
- onChange={onToggleTable}
- />
- ))}
-
+
+
+
+
+
+ );
+
+ return (
+
+ !open && closeSelectorModal()}
+ placement="bottomLeft"
+ trigger="click"
+ >
+ }
+ sqlLabMode
+ onOpenModal={openSelectorModal}
+ />
+
+
+
{shouldShowReset && (