mirror of https://github.com/apache/superset.git
build: enable typescript for cypress (#10170)
This commit is contained in:
parent
539e11b67e
commit
4342c33d0e
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["cypress", "@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-unresolved": 0,
|
||||
"@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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"plugins": [
|
||||
"cypress"
|
||||
],
|
||||
"env": {
|
||||
"cypress/globals": true
|
||||
}
|
||||
}
|
|
@ -18,11 +18,23 @@
|
|||
*/
|
||||
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
||||
|
||||
describe('Dashboard filter', () => {
|
||||
let filterId;
|
||||
let aliases;
|
||||
interface Slice {
|
||||
slice_id: number;
|
||||
form_data: {
|
||||
viz_type: string;
|
||||
[key: string]: JSONValue;
|
||||
};
|
||||
}
|
||||
|
||||
const getAlias = id => {
|
||||
interface DashboardData {
|
||||
slices: Slice[];
|
||||
}
|
||||
|
||||
describe('Dashboard filter', () => {
|
||||
let filterId: number;
|
||||
let aliases: string[];
|
||||
|
||||
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',
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
/// <reference types="cypress" />
|
||||
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';
|
|
@ -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
|
||||
});
|
|
@ -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;
|
||||
},
|
||||
);
|
|
@ -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<ReturnType<JSON['parse']>>(resolve => {
|
||||
if (blob instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(JSON.parse(String(reader.result || '')));
|
||||
reader.readAsText(blob);
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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"]
|
||||
}
|
Loading…
Reference in New Issue