build: enable typescript for cypress (#10170)

This commit is contained in:
Jesse Yang 2020-06-29 10:53:33 -07:00 committed by GitHub
parent 539e11b67e
commit 4342c33d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 187 additions and 154 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -1,8 +0,0 @@
{
"plugins": [
"cypress"
],
"env": {
"cypress/globals": true
}
}

View File

@ -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',

View File

@ -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');
});

View File

@ -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 => {

View File

@ -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,
);

View File

@ -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)

View File

@ -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';

View File

@ -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
});

View File

@ -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;
},
);

View File

@ -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);
}
});
}

View File

@ -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"]
}