diff --git a/.github/workflows/prefer_typescript.yml b/.github/workflows/prefer_typescript.yml
index db1c926cbbd02..2572ef3b15fe8 100644
--- a/.github/workflows/prefer_typescript.yml
+++ b/.github/workflows/prefer_typescript.yml
@@ -21,10 +21,7 @@ jobs:
js_files_added() {
jq -r '
map(
- select(
- (contains("cypress-base/") | not) and
- (endswith(".js") or endswith(".jsx"))
- )
+ select((endswith(".js") or endswith(".jsx"))
) | join("\n")
' ${HOME}/files_added.json
}
diff --git a/superset-frontend/cypress-base/.eslintrc b/superset-frontend/cypress-base/.eslintrc
new file mode 100644
index 0000000000000..9d1b5a5c0a841
--- /dev/null
+++ b/superset-frontend/cypress-base/.eslintrc
@@ -0,0 +1,24 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["cypress", "@typescript-eslint"],
+ "extends": [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:cypress/recommended"
+ ],
+ "rules": {
+ "@typescript-eslint/explicit-function-return-type": 0,
+ "@typescript-eslint/explicit-module-boundary-types": 0,
+ "@typescript-eslint/no-var-requires": 0,
+ "@typescript-eslint/camelcase": 0
+ },
+ "settings": {
+ "import/resolver": {
+ "node": {
+ "extensions": [".js", ".jsx", ".ts", ".tsx"]
+ }
+ }
+ },
+ "env": {
+ "cypress/globals": true
+ }
+}
diff --git a/superset-frontend/cypress-base/cypress.json b/superset-frontend/cypress-base/cypress.json
index 76e4778f9c7f6..8856588dfa42a 100644
--- a/superset-frontend/cypress-base/cypress.json
+++ b/superset-frontend/cypress-base/cypress.json
@@ -2,9 +2,10 @@
"baseUrl": "http://localhost:8081",
"chromeWebSecurity": false,
"defaultCommandTimeout": 5000,
+ "experimentalFetchPolyfill": true,
"requestTimeout": 10000,
"ignoreTestFiles": [
- "**/!(*.test.js)"
+ "**/!(*.test.js|*.test.ts)"
],
"video": false,
"videoUploadOnPasses": false,
diff --git a/superset-frontend/cypress-base/cypress/.eslintrc b/superset-frontend/cypress-base/cypress/.eslintrc
deleted file mode 100644
index 5b988562725d3..0000000000000
--- a/superset-frontend/cypress-base/cypress/.eslintrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "plugins": [
- "cypress"
- ],
- "env": {
- "cypress/globals": true
- }
-}
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
similarity index 79%
rename from superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.js
rename to superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
index d734a571561d9..6f386000de96a 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
@@ -18,11 +18,23 @@
*/
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
+interface Slice {
+ slice_id: number;
+ form_data: {
+ viz_type: string;
+ [key: string]: JSONValue;
+ };
+}
+
+interface DashboardData {
+ slices: Slice[];
+}
+
describe('Dashboard filter', () => {
- let filterId;
- let aliases;
+ let filterId: number;
+ let aliases: string[];
- const getAlias = id => {
+ const getAlias = (id: number) => {
return `@slice_${id}`;
};
@@ -32,13 +44,14 @@ describe('Dashboard filter', () => {
cy.visit(WORLD_HEALTH_DASHBOARD);
- cy.get('#app').then(data => {
- const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
- const dashboard = bootstrapData.dashboard_data;
+ cy.get('#app').then(app => {
+ const bootstrapData = app.data('bootstrap');
+ const dashboard = bootstrapData.dashboard_data as DashboardData;
const sliceIds = dashboard.slices.map(slice => slice.slice_id);
- filterId = dashboard.slices.find(
- slice => slice.form_data.viz_type === 'filter_box',
- ).slice_id;
+ filterId =
+ dashboard.slices.find(
+ slice => slice.form_data.viz_type === 'filter_box',
+ )?.slice_id || 0;
aliases = sliceIds.map(id => {
const alias = getAlias(id);
const url = `/superset/explore_json/?*{"slice_id":${id}}*`;
@@ -72,7 +85,7 @@ describe('Dashboard filter', () => {
cy.get('.Select__control input[type=text]')
.first()
- .focus({ force: true })
+ .focus()
.type('So', { force: true });
cy.get('.Select__menu').first().contains('Create "So"');
@@ -81,7 +94,7 @@ describe('Dashboard filter', () => {
// we refocus the input again here. The is not happening in real life.
cy.get('.Select__control input[type=text]')
.first()
- .focus({ force: true })
+ .focus()
.type('uth Asia{enter}', { force: true });
// by default, need to click Apply button to apply filter
@@ -90,8 +103,10 @@ describe('Dashboard filter', () => {
// wait again after applied filters
cy.wait(aliases.filter(x => x !== getAlias(filterId))).then(requests => {
requests.forEach(xhr => {
- const requestFormData = xhr.request.body;
- const requestParams = JSON.parse(requestFormData.get('form_data'));
+ const requestFormData = xhr.request.body as FormData;
+ const requestParams = JSON.parse(
+ requestFormData.get('form_data') as string,
+ );
expect(requestParams.extra_filters[0]).deep.eq({
col: 'region',
op: 'in',
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js
index 8f7069ccb2c9c..650d372232ba6 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js
@@ -21,7 +21,6 @@ import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
describe('Dashboard save action', () => {
let dashboardId;
- let boxplotChartId;
beforeEach(() => {
cy.server();
@@ -32,10 +31,6 @@ describe('Dashboard save action', () => {
const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
const dashboard = bootstrapData.dashboard_data;
dashboardId = dashboard.id;
- boxplotChartId = dashboard.slices.find(
- slice => slice.form_data.viz_type === 'box_plot',
- ).slice_id;
-
cy.route('POST', `/superset/copy_dash/${dashboardId}/`).as('copyRequest');
});
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.js b/superset-frontend/cypress-base/cypress/integration/explore/link.test.js
index e8bc56cedb367..1d0ce87913140 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.js
@@ -55,8 +55,6 @@ describe('Test explore links', () => {
// explicitly wait for the url response
cy.wait('@getShortUrl');
- cy.wait(100);
-
cy.get('#shorturl-popover [data-test="short-url"]')
.invoke('text')
.then(text => {
diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js
index 43e9706c25d59..d876ef637fc83 100644
--- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js
@@ -24,30 +24,28 @@ describe('SqlLab query tabs', () => {
});
it('allows you to create a tab', () => {
- cy.get('#a11y-query-editor-tabs > ul > li').then(tabList => {
+ cy.get('.SqlEditorTabs > ul > li').then(tabList => {
const initialTabCount = tabList.length;
-
// add tab
- cy.get('#a11y-query-editor-tabs > ul > li').last().click();
-
- cy.get('#a11y-query-editor-tabs > ul > li').should(
- 'have.length',
- initialTabCount + 1,
+ cy.get('.SqlEditorTabs > ul > li').last().click();
+ // wait until we find the new tab
+ cy.get(`.SqlEditorTabs > ul > li:eq(${initialTabCount - 1})`).contains(
+ 'Untitled Query',
);
});
});
it('allows you to close a tab', () => {
- cy.get('#a11y-query-editor-tabs > ul > li').then(tabListA => {
+ cy.get('.SqlEditorTabs > ul > li').then(tabListA => {
const initialTabCount = tabListA.length;
// open the tab dropdown to remove
- cy.get('#a11y-query-editor-tabs > ul > li .dropdown-toggle').click();
+ cy.get('.SqlEditorTabs > ul > li .dropdown-toggle').click();
// first item is close
- cy.get('#a11y-query-editor-tabs .close-btn a').click();
+ cy.get('.SqlEditorTabs .close-btn a').click();
- cy.get('#a11y-query-editor-tabs > ul > li').should(
+ cy.get('.SqlEditorTabs > ul > li').should(
'have.length',
initialTabCount - 1,
);
diff --git a/superset-frontend/cypress-base/cypress/plugins/index.js b/superset-frontend/cypress-base/cypress/plugins/index.js
index adfeabedbc284..4df323f823748 100644
--- a/superset-frontend/cypress-base/cypress/plugins/index.js
+++ b/superset-frontend/cypress-base/cypress/plugins/index.js
@@ -16,16 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
diff --git a/superset-frontend/cypress-base/cypress/support/index.d.ts b/superset-frontend/cypress-base/cypress/support/index.d.ts
new file mode 100644
index 0000000000000..80a936ef407fb
--- /dev/null
+++ b/superset-frontend/cypress-base/cypress/support/index.d.ts
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// eslint-disable-next-line spaced-comment
+///
+type JSONPrimitive = string | number | boolean | null;
+type JSONValue = JSONPrimitive | JSONObject | JSONArray;
+type JSONObject = { [member: string]: JSONValue };
+type JSONArray = JSONValue[];
+
+declare namespace Cypress {
+ interface Chainable {
+ /**
+ * Login test user.
+ */
+ login(): void;
+
+ /**
+ * Verify a waitXHR response and parse response JSON.
+ */
+ verifyResponseCodes(
+ xhr: WaitXHR,
+ callback?: (result: JSONValue) => void,
+ ): cy;
+
+ /**
+ * Verify slice container renders.
+ */
+ verifySliceContainer(chartSelector: JQuery.Selector): cy;
+
+ /**
+ * Verify slice successfully loaded.
+ */
+ verifySliceSuccess({
+ waitAlias,
+ querySubString,
+ chartSelector,
+ }: {
+ waitAlias: string;
+ querySubString: string;
+ chartSelector: JQuery.Selector;
+ }): cy;
+ }
+}
+
+declare module '@cypress/code-coverage/task';
diff --git a/superset-frontend/cypress-base/cypress/support/index.js b/superset-frontend/cypress-base/cypress/support/index.js
deleted file mode 100644
index 52bd671616a7f..0000000000000
--- a/superset-frontend/cypress-base/cypress/support/index.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-import '@cypress/code-coverage/support';
-import './commands';
-
-// The following is a workaround for Cypress not supporting fetch.
-// By setting window.fetch = null, we force the fetch polyfill to fall back
-// to xhr as described here https://github.com/cypress-io/cypress/issues/95
-Cypress.on('window:before:load', win => {
- win.fetch = null; // eslint-disable-line no-param-reassign
-});
diff --git a/superset-frontend/cypress-base/cypress/support/commands.js b/superset-frontend/cypress-base/cypress/support/index.ts
similarity index 52%
rename from superset-frontend/cypress-base/cypress/support/commands.js
rename to superset-frontend/cypress-base/cypress/support/index.ts
index fafff6cb94b9f..ad70b732e5709 100644
--- a/superset-frontend/cypress-base/cypress/support/commands.js
+++ b/superset-frontend/cypress-base/cypress/support/index.ts
@@ -16,32 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add("login", (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This is will overwrite an existing command --
-// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
-
+import '@cypress/code-coverage/support';
import readResponseBlob from '../utils/readResponseBlob';
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
@@ -63,47 +38,60 @@ Cypress.Commands.add('visitChartByName', name => {
});
Cypress.Commands.add('visitChartById', chartId => {
- cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`);
+ return cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`);
});
Cypress.Commands.add('visitChartByParams', params => {
- cy.visit(`${BASE_EXPLORE_URL}${params}`);
+ return cy.visit(`${BASE_EXPLORE_URL}${params}`);
});
-Cypress.Commands.add('verifyResponseCodes', async xhr => {
+Cypress.Commands.add('verifyResponseCodes', (xhr: XMLHttpRequest, callback) => {
// After a wait response check for valid response
expect(xhr.status).to.eq(200);
-
- const responseBody = await readResponseBlob(xhr.response.body);
-
- if (responseBody.error) {
- expect(responseBody.error).to.eq(null);
- }
+ readResponseBlob(xhr.response.body).then(res => {
+ expect(res).to.not.be.instanceOf(Error);
+ if (callback) {
+ callback(res);
+ }
+ });
+ return cy;
});
Cypress.Commands.add('verifySliceContainer', chartSelector => {
// After a wait response check for valid slice container
- cy.get('.slice_container').within(async () => {
+ cy.get('.slice_container').within(() => {
if (chartSelector) {
- const chart = await cy.get(chartSelector);
- expect(chart[0].clientWidth).greaterThan(0);
- expect(chart[0].clientHeight).greaterThan(0);
+ cy.get(chartSelector).then(chart => {
+ expect(chart[0].clientWidth).greaterThan(0);
+ expect(chart[0].clientHeight).greaterThan(0);
+ });
}
});
+ return cy;
});
Cypress.Commands.add(
'verifySliceSuccess',
- ({ waitAlias, querySubstring, chartSelector }) => {
- cy.wait(waitAlias).then(async xhr => {
- cy.verifyResponseCodes(xhr);
-
- const responseBody = await readResponseBlob(xhr.response.body);
- if (querySubstring) {
- expect(responseBody.query).contains(querySubstring);
- }
-
+ ({
+ waitAlias,
+ querySubstring,
+ chartSelector,
+ }: {
+ waitAlias: string;
+ querySubstring: string;
+ chartSelector: JQuery.Selector;
+ }) => {
+ cy.wait(waitAlias).then(xhr => {
cy.verifySliceContainer(chartSelector);
+ cy.verifyResponseCodes(xhr, responseBody => {
+ if (querySubstring) {
+ type QueryResponse = { query: string };
+ expect(
+ responseBody && (responseBody as QueryResponse).query,
+ ).contains(querySubstring);
+ }
+ });
});
+ return cy;
},
);
diff --git a/superset-frontend/cypress-base/cypress/utils/readResponseBlob.js b/superset-frontend/cypress-base/cypress/utils/readResponseBlob.ts
similarity index 63%
rename from superset-frontend/cypress-base/cypress/utils/readResponseBlob.js
rename to superset-frontend/cypress-base/cypress/utils/readResponseBlob.ts
index 6cf077b37ab55..560a5c55d54b6 100644
--- a/superset-frontend/cypress-base/cypress/utils/readResponseBlob.js
+++ b/superset-frontend/cypress-base/cypress/utils/readResponseBlob.ts
@@ -16,14 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-// This function returns a promise that resolves to the value
-// of the passed response blob. It assumes the blob should be read as text,
-// and that the response can be parsed as JSON. This is needed to read
-// the value of any fetch-based response.
-export default function readResponseBlob(blob) {
- return new Promise(resolve => {
- const reader = new FileReader();
- reader.onload = () => resolve(JSON.parse(reader.result));
- reader.readAsText(blob);
+/**
+ * Read XHR response and parse it as JSON.
+ */
+export default function readResponseBlob(blob: Blob | JSONValue) {
+ return new Promise>(resolve => {
+ if (blob instanceof Blob) {
+ const reader = new FileReader();
+ reader.onload = () => resolve(JSON.parse(String(reader.result || '')));
+ reader.readAsText(blob);
+ } else {
+ resolve(blob);
+ }
});
}
diff --git a/superset-frontend/cypress-base/tsconfig.json b/superset-frontend/cypress-base/tsconfig.json
new file mode 100644
index 0000000000000..eec99e45f9eb5
--- /dev/null
+++ b/superset-frontend/cypress-base/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "ES5",
+ "lib": ["ES5", "ES2015", "DOM"],
+ "types": ["cypress"],
+ "allowJs": true,
+ "noEmit": true
+ },
+ "files": ["cypress/support/index.d.ts"],
+ "include": ["node_modules/cypress", "cypress/**/*.ts"]
+}