diff --git a/Makefile b/Makefile index 5e983309f1..4928064593 100755 --- a/Makefile +++ b/Makefile @@ -81,6 +81,6 @@ test_e2e_compat_features: test_e2e_common_features: cd ui &&\ - yarn run:e2e-test:common-features TIDB_VERSION=$(TIDB_VERSION) + yarn run:e2e-test:common-features --env TIDB_VERSION=$(TIDB_VERSION) test_e2e: test_e2e_compat_features test_e2e_common_features \ No newline at end of file diff --git a/ui/cypress/fixtures/uri.json b/ui/cypress/fixtures/uri.json index bfb6063b0b..196880c1c3 100644 --- a/ui/cypress/fixtures/uri.json +++ b/ui/cypress/fixtures/uri.json @@ -2,5 +2,7 @@ "root": "/", "login": "/signin", "overview": "/overview", - "slow_query": "/slow_query" + "slow_query": "/slow_query", + "statement": "/statement", + "configuration": "/configuration" } diff --git a/ui/cypress/integration/components.js b/ui/cypress/integration/components.js new file mode 100644 index 0000000000..fce38258d6 --- /dev/null +++ b/ui/cypress/integration/components.js @@ -0,0 +1,29 @@ +// Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0. + +export const testBaseSelectorOptions = (optionsList, dataE2EValue) => { + cy.get(`[data-e2e=${dataE2EValue}]`) + .click() + .then(() => { + cy.get('[data-e2e=multi_select_options]') + .should('have.length', optionsList.length) + .each(($option, $idx) => { + cy.wrap($option).should('have.text', optionsList[$idx]) + }) + }) +} + +export const checkAllOptionsInBaseSelector = (dataE2EValue) => { + cy.get(`[data-e2e=${dataE2EValue}]`) + .click() + .then(() => { + if (cy.get('.ant-dropdown').should('exist')) { + cy.get('.ant-dropdown').within(() => { + cy.get('[role=columnheader]') + .eq(0) + .within(() => { + cy.get('.ant-checkbox').click() + }) + }) + } + }) +} diff --git a/ui/cypress/integration/slow_query/01-list.spec.js b/ui/cypress/integration/slow_query/01-list.spec.js index 7c9e246836..1a43e97482 100644 --- a/ui/cypress/integration/slow_query/01-list.spec.js +++ b/ui/cypress/integration/slow_query/01-list.spec.js @@ -1,7 +1,12 @@ // Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0. import dayjs from 'dayjs' -import { validateCSVList, deleteDownloadsFolder } from '../utils' +import { + restartTiUP, + validateSlowQueryCSVList, + deleteDownloadsFolder, +} from '../utils' +import { testBaseSelectorOptions } from '../components' const neatCSV = require('neat-csv') const path = require('path') @@ -13,13 +18,7 @@ describe('SlowQuery list page', () => { }) // Restart tiup - cy.exec( - `bash ../scripts/start_tiup.sh ${Cypress.env('TIDB_VERSION')} restart`, - { log: true } - ) - - // Wait TiUP Playground - cy.exec('bash ../scripts/wait_tiup_playground.sh 1 300 &> wait_tiup.log') + restartTiUP() deleteDownloadsFolder() }) @@ -206,7 +205,10 @@ describe('SlowQuery list page', () => { describe('Filter slow query by changing database', () => { it('No database selected by default', () => { - cy.get('[data-e2e=base_select_input]').should('has.text', '') + cy.get('[data-e2e=base_select_input_text]').should( + 'has.text', + 'All Databases' + ) }) it('Show all databases', () => { @@ -216,14 +218,7 @@ describe('SlowQuery list page', () => { cy.wait('@databases').then((res) => { const databaseList = res.response.body - cy.get('[data-e2e=base_selector]') - .click() - .then(() => { - cy.get('[data-e2e=multi_select_options]').should( - 'have.length', - databaseList.length - ) - }) + testBaseSelectorOptions(databaseList, 'execution_database_name') }) }) @@ -239,11 +234,11 @@ describe('SlowQuery list page', () => { // global and use database queries will be listed cy.get('[data-automation-key=query]').should('has.length', 3) - cy.get('[data-e2e=base_select_input]') - .click() + cy.get('[data-e2e=base_select_input_text]') + .click({ force: true }) .then(() => { cy.get('.ms-DetailsHeader-checkTooltip') - .click() + .click({ force: true }) .then(() => { // global query will not be listed cy.get('[data-automation-key=query]').should('has.length', 2) @@ -308,7 +303,8 @@ describe('SlowQuery list page', () => { .eq(1) .click() .then(() => { - cy.get('[data-automation-key=query]').should('has.length', 3) + cy.reload() + cy.get('[data-e2e=slow_query_limit_select]').contains('200') }) }) }) @@ -331,7 +327,7 @@ describe('SlowQuery list page', () => { }) }) - it('Hover on columns selector and check selected fileds ', () => { + it('Hover on columns selector and check selected fields ', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { @@ -352,11 +348,11 @@ describe('SlowQuery list page', () => { }) }) - it('Select all column fileds', () => { + it('Select all column fields', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { - cy.get('[data-e2e=slow_query_schema_table_column_tile]') + cy.get('[data-e2e=column_selector_title]') .check() .then(() => { cy.get('[role=columnheader]') @@ -370,7 +366,7 @@ describe('SlowQuery list page', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { - cy.get('[data-e2e=slow_query_schema_table_column_reset]') + cy.get('[data-e2e=column_selector_reset]') .click() .then(() => { cy.get('[role=columnheader]') @@ -422,7 +418,7 @@ describe('SlowQuery list page', () => { .then(() => { cy.get('[data-automation-key=query]') .eq(0) - .find('[data-e2e=text_wrap_multiline]') + .find('[data-e2e=syntax_highlighter_original]') }) cy.get('[data-e2e=slow_query_show_full_sql]') @@ -431,7 +427,7 @@ describe('SlowQuery list page', () => { cy.get('[data-automation-key=query]') .eq(0) .trigger('mouseover') - .find('[data-e2e=text_wrap_singleline_with_tooltip]') + .find('[data-e2e=syntax_highlighter_compact]') }) }) }) @@ -581,7 +577,7 @@ describe('SlowQuery list page', () => { cy.readFile(downloadedFilename, { timeout: 15000 }) // parse CSV text into objects .then(neatCSV) - .then(validateCSVList) + .then(validateSlowQueryCSVList) }) }) }) diff --git a/ui/cypress/integration/slow_query/02-detail.spec.js b/ui/cypress/integration/slow_query/02-detail.spec.js index 5e4370d067..f1afd51eda 100644 --- a/ui/cypress/integration/slow_query/02-detail.spec.js +++ b/ui/cypress/integration/slow_query/02-detail.spec.js @@ -20,7 +20,7 @@ describe('Slow query detail page E2E test', () => { it('Check sql and default format', () => { // sql is collapsed by default cy.get('[data-e2e=expandText]').eq(0).should('have.text', 'Expand') - cy.get('[data-e2e=slow_query_detail_page_query]') + cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_compact]') .and('have.text', 'SELECT sleep(1.2);') @@ -32,7 +32,7 @@ describe('Slow query detail page E2E test', () => { // sql is collapsed by default cy.get('[data-e2e=collapseText]').eq(0).should('have.text', 'Collapse') - cy.get('[data-e2e=slow_query_detail_page_query]') + cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_original]') .and('have.text', 'SELECT\n sleep(1.2);') @@ -79,7 +79,7 @@ describe('Slow query detail page E2E test', () => { cy.wait('@slow_query_detail').then((res) => { const responseBody = res.response.body - cy.get('[data-e2e=slow_query_detail_page_query]') + cy.get('[data-e2e=statement_query_detail_page_query]') .eq(1) .and('have.text', responseBody.plan) }) diff --git a/ui/cypress/integration/statement/01-list.spec.js b/ui/cypress/integration/statement/01-list.spec.js new file mode 100644 index 0000000000..6c9306fbb7 --- /dev/null +++ b/ui/cypress/integration/statement/01-list.spec.js @@ -0,0 +1,892 @@ +// Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0. + +import dayjs from 'dayjs' + +import { + restartTiUP, + validateStatementCSVList, + deleteDownloadsFolder, +} from '../utils' +import { + testBaseSelectorOptions, + checkAllOptionsInBaseSelector, +} from '../components' + +const neatCSV = require('neat-csv') +const path = require('path') + +describe('SQL statements list page', () => { + before(() => { + cy.fixture('uri.json').then(function (uri) { + this.uri = uri + }) + + restartTiUP() + + deleteDownloadsFolder() + }) + + beforeEach(function () { + cy.login('root') + cy.visit(this.uri.statement) + cy.url().should('include', this.uri.statement) + }) + + const defaultExecStmtList = [ + 'SHOW DATABASES', + 'SELECT DISTINCT `stmt_type` FROM `information_schema`.`cluster_statements_summary_history` ORDER BY `stmt_type` ASC', + 'SELECT `version` ()', + ] + + describe('Initialize statement list page', () => { + it('Statement side bar highlighted', () => { + cy.get('[data-e2e=menu_item_statement]') + .should('be.visible') + .and('has.class', 'ant-menu-item-selected') + }) + + it('Has Toolbar', function () { + cy.get('[data-e2e=statement_toolbar]').should('be.visible') + }) + + it('Statements is enabled by default', () => { + cy.get('[data-e2e=statements_table]').should('be.visible') + }) + + it('Get statement list bad request', () => { + const staticResponse = { + statusCode: 400, + body: { + code: 'common.bad_request', + error: true, + message: 'common.bad_request', + }, + } + + // stub out a response body + cy.intercept( + `${Cypress.env('apiBasePath')}statements/list*`, + staticResponse + ).as('statements_list') + cy.wait('@statements_list').then(() => { + cy.get('[data-e2e=alert_error_bar]').should( + 'has.text', + staticResponse.body.message + ) + }) + }) + + it('Statements which executed by default when starting TiDB', () => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + + cy.wait('@statements_list').then((res) => { + const response = res.response.body + + cy.get('[data-e2e=syntax_highlighter_compact]') + .should('have.length', response.length) + .then(($stmts) => { + // we get a list of jQuery elements + // let's convert the jQuery object into a plain array + return ( + Cypress.$.makeArray($stmts) + // and extract inner text from each + .map((stmt) => stmt.innerText) + ) + }) + // make sure there exists the default executed statements + .should('to.include.members', defaultExecStmtList) + }) + }) + }) + + describe('Time range selector', () => { + beforeEach(() => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'init_statements_list' + ) + + cy.wait('@init_statements_list') + + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list_with_last_seen_field' + ) + + // select last_seen column field + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.contains('Last Seen').within(() => { + cy.get('[data-e2e=columns_selector_field_last_seen]').check({ + force: true, + }) + }) + }) + }) + + const getNearTime = () => { + const cur = dayjs() + let endTime, startTime + if (cur.get('minute') > 30) { + endTime = dayjs( + cur + .set('hour', cur.get('hour') + 1) + .set('minute', 0) + .set('second', 0) + ).unix() + startTime = dayjs(cur.set('minute', 30).set('second', 0)).unix() + } else { + endTime = dayjs(cur.set('minute', 30).set('second', 0)).unix() + startTime = dayjs(cur.set('minute', 0).set('second', 0)).unix() + } + return [startTime, endTime] + } + + const checkStmtListWithTimeRange = (stmtList, timeDiff) => { + const now = dayjs().unix() + + stmtList.forEach((stmt) => { + cy.wrap(stmt.last_seen) + .should('be.lte', now) + .and('be.gt', now - timeDiff) + }) + } + + describe('Common time range selector', () => { + it('Default time range', () => { + cy.get('[data-e2e=statement_timerange_selector]').should( + 'have.text', + 'Recent 30 min' + ) + }) + + it('Common time range options', () => { + cy.get('[data-e2e=statement_timerange_selector]') + .click() + .then(() => { + cy.get('[data-e2e=statement_time_range_option]') + .should('have.length', 12) + .each(($option, $idx) => { + if ($idx == 0) { + // Recent 15 min is enabled + cy.wrap($option) + .invoke('attr', 'class') + .should('not.contain', 'time_range_item_disabled') + } else if ($idx == 1) { + // Recent 30 min is active + cy.wrap($option) + .invoke('attr', 'class') + .should('contain', 'time_range_item_active') + } else { + // the remained options are disabled + cy.wrap($option) + .invoke('attr', 'class') + .should('contain', 'time_range_item_disabled') + } + }) + }) + }) + + it('Custom time range selector', () => { + const [startTime, endTime] = getNearTime() + cy.get('[data-e2e=statement_timerange_selector]') + .click() + .then(() => { + cy.get('.ant-slider').within(() => { + cy.get('[role=slider]') + .eq(0) + .should('have.attr', 'aria-valuemin', startTime) + .and('have.attr', 'aria-valuemax', endTime) + cy.get('[role=slider]') + .eq(1) + .should('have.attr', 'aria-valuemin', startTime) + .and('have.attr', 'aria-valuemax', endTime) + }) + }) + }) + + it('Init statement list', () => { + cy.wait('@statements_list_with_last_seen_field').then((res) => { + const response = res.response.body + + cy.get('[data-automation-key=digest_text]').should( + 'have.length', + response.length + ) + + checkStmtListWithTimeRange(response, 1800) + }) + }) + + it('Select time range as recent 15 mins', () => { + // select recent 15 mins + cy.get('[data-e2e=statement_timerange_selector]') + .click() + .then(() => { + cy.get('[data-e2e=statement_time_range_option]').eq(0).click() + }) + + cy.wait('@statements_list_with_last_seen_field').then((res) => { + const response = res.response.body + checkStmtListWithTimeRange(response, 900) + }) + + // time rage will be remebered after reload page + cy.reload() + cy.get('[data-e2e=statement_timerange_selector]').should( + 'have.text', + 'Recent 15 min' + ) + }) + }) + }) + + describe('Filter statements by changing database', () => { + it('No database selected by default', () => { + cy.get('[data-e2e=base_select_input_text]') + .eq(0) + .should('has.text', 'All Databases') + }) + + it('Show all databases', () => { + cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( + 'databases' + ) + + cy.wait('@databases').then((res) => { + const databases = res.response.body + testBaseSelectorOptions(databases, 'execution_database_name') + }) + }) + + it('Filter statements without use database', () => { + cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( + 'databases' + ) + + cy.wait('@databases').then(() => { + // check all options in databases selector + + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + + checkAllOptionsInBaseSelector('execution_database_name') + + cy.wait('@statements_list').then(() => { + // check the existence of statements without use database + cy.contains(defaultExecStmtList[0]).should('not.exist') + cy.contains(defaultExecStmtList[2]).should('not.exist') + }) + }) + }) + + it('Filter statements with use database (mysql)', () => { + cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( + 'databases' + ) + + let queryData = { + query: 'SELECT count(*) from user;', + database: 'mysql', + } + cy.task('queryDB', { ...queryData }) + + cy.wait('@databases').then(() => { + cy.get('[data-e2e=execution_database_name]') + .eq(0) + .click() + .then(() => { + cy.get('.ant-dropdown').within(() => { + cy.get('.ant-checkbox-input').eq(3).click() + }) + }) + .then(() => { + cy.contains('SELECT count (?) FROM user;').should('exist') + }) + }) + + // Use databases config remembered + cy.reload() + cy.get('[data-e2e=base_select_input_text]') + .eq(0) + .should('has.text', '1 Databases') + }) + }) + + describe('Filter statements by changing kind', () => { + it('No kind selected by default', () => { + cy.get('[data-e2e=base_select_input_text]') + .eq(1) + .should('has.text', 'All Kinds') + }) + + it('Show all kind of statements', () => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( + 'stmt_types' + ) + + cy.wait('@stmt_types').then((res) => { + const stmtTypesList = res.response.body + testBaseSelectorOptions(stmtTypesList, 'statement_types') + }) + }) + + it('Filter statements with all kind checked', () => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( + 'stmt_types' + ) + + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + + cy.wait(['@stmt_types', '@statements_list']).then((interceptions) => { + // check all options in kind selector + checkAllOptionsInBaseSelector('statement_types') + const statementsList = interceptions[1].response.body + cy.get('[data-e2e=syntax_highlighter_compact]').should( + 'have.length', + statementsList.length + ) + }) + }) + + it('Filter statements with one kind checked (select)', () => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( + 'stmt_types' + ) + + cy.wait('@stmt_types').then(() => { + cy.get('[data-e2e=statement_types]') + .click() + .then(() => { + cy.get('.ant-dropdown').within(() => { + cy.get('[data-e2e=multi_select_options]') + .contains('Select') + .click({ force: true }) + }) + }) + .then(() => { + cy.get('[data-e2e=syntax_highlighter_compact]').each(($sql) => { + cy.wrap($sql).contains('SELECT') + }) + }) + }) + }) + }) + + describe('Search function', () => { + it('Default search text', () => { + cy.get('[data-e2e=sql_statements_search]').should('be.empty') + }) + + // test will fail caused by existing TiDB bug + // https://github.com/pingcap/tidb/issues/32783 + it('Search item with space', () => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + cy.get('[data-e2e=sql_statements_search]').type(' SELECT version{enter}') + cy.wait('@statements_list').then(() => { + cy.get('[data-e2e=syntax_highlighter_compact]').each(($stmt) => { + cy.wrap($stmt).contains('SELECT') + }) + }) + + // check search text remembered after reload page + + cy.reload() + cy.wait('@statements_list').then(() => { + cy.get('[data-e2e=syntax_highlighter_compact]').each(($stmt) => { + cy.wrap($stmt).contains('SELECT') + }) + }) + }) + + it('Type search without pressing enter then reload', () => { + cy.get('[data-e2e=sql_statements_search]').type('SELECT \\`version\\` ()') + + cy.reload() + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + + cy.get('[data-e2e=sql_statements_search]').clear().type('{enter}') + + cy.wait('@statements_list').then((res) => { + const statementsList = res.response.body + cy.get('[data-e2e=syntax_highlighter_compact]').should( + 'has.length', + statementsList.length + ) + }) + }) + }) + + describe('Selected Columns', () => { + const defaultColumns = { + digest_text: 'Statement Template ', + sum_latency: 'Total Latency ', + avg_latency: 'Mean Latency ', + exec_count: '# Exec ', + plan_count: '# Plans ', + } + + it('Default selected columns', () => { + cy.get('[role=columnheader]') + .not('.is-empty') + .should('have.length', 5) + .each(($column, idx) => { + cy.wrap($column).contains( + defaultColumns[Object.keys(defaultColumns)[idx]] + ) + }) + }) + + it('Hover on columns selector and check selected fields', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.get('[data-e2e=columns_selector_popover_content]') + .should('be.visible') + .within(() => { + cy.get('.ant-checkbox-wrapper-checked') + // .should('have.length', 5) + .then(($options) => { + return Cypress.$.makeArray($options).map( + (option) => option.innerText + ) + }) + // make sure there exists the default executed statements + .should('to.deep.eq', Object.values(defaultColumns)) + }) + }) + }) + + it('Select all column fields', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.get('[data-e2e=column_selector_title]') + .check() + .then(() => { + cy.get('[role=columnheader]') + .not('.is-empty') + .should('have.length', 43) + }) + }) + }) + + it('Reset selected column fields', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.get('[data-e2e=column_selector_reset]') + .click() + .then(() => { + cy.get('[role=columnheader]') + .not('.is-empty') + .should('have.length', 5) + }) + }) + }) + + it('Select an orbitary column field', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.contains('Total Coprocessor Tasks') + .within(() => { + cy.get( + '[data-e2e=columns_selector_field_sum_cop_task_num]' + ).check() + }) + .then(() => { + cy.get('[data-item-key=sum_cop_task_num]').should( + 'have.text', + 'Total Coprocessor Tasks' + ) + }) + }) + }) + + it('UnCheck last selected orbitary column field', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover') + .then(() => { + cy.contains('Total Coprocessor Tasks') + .within(() => { + cy.get( + '[data-e2e=columns_selector_field_sum_cop_task_num]' + ).uncheck() + }) + .then(() => { + cy.get('[data-item-key=sum_cop_task_num]').should('not.exist') + }) + }) + }) + + it('Check SHOW_FULL_QUERY_TEXT', () => { + cy.get('[data-e2e=columns_selector_popover]') + .trigger('mouseover', { force: true }) + .then(() => { + cy.get('[data-e2e=statement_show_full_sql]') + .check() + .then(() => { + cy.get('[data-automation-key=digest_text]') + .eq(0) + .find('[data-e2e=syntax_highlighter_original]') + }) + + cy.get('[data-e2e=statement_show_full_sql]') + .uncheck() + .then(() => { + cy.get('[data-automation-key=digest_text]') + .eq(0) + .trigger('mouseover', { force: true }) + .find('[data-e2e=syntax_highlighter_compact]') + }) + }) + }) + }) + + describe('Reload statement', () => { + it('Reload statement table after execute a query', () => { + let queryData = { + query: 'select count(*) from tidb;', + database: 'mysql', + } + cy.task('queryDB', { ...queryData }) + + cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( + 'statements_list' + ) + cy.wait('@statements_list').then(() => { + cy.get('[data-e2e=statement_refresh]') + .click() + .then(() => { + cy.get('[data-automation-key=digest_text]').contains( + 'SELECT count (?) FROM tidb;' + ) + }) + }) + }) + }) + + const calcStmtHistorySize = (refreshInterval, historySize) => { + const totalMins = refreshInterval * historySize + const day = Math.floor(totalMins / (24 * 60)) + const hour = Math.floor((totalMins - day * 24 * 60) / 60) + const min = totalMins - day * 24 * 60 - hour * 60 + return `${day} day ${hour} hour ${min} min` + } + + describe('Statement Setting', function () { + it('Close setting panel', () => { + // close panel by clicking mask + cy.get('[data-e2e=statement_setting]') + .click() + .then(() => { + cy.get('.ant-drawer-mask') + .click() + .then(() => { + cy.get('.ant-drawer-content').should('not.be.visible') + }) + }) + + // close panel by clicking close icon + cy.get('[data-e2e=statement_setting]') + .click() + .then(() => { + cy.get('.ant-drawer-close') + .click() + .then(() => { + cy.get('.ant-drawer-content').should('not.be.visible') + }) + }) + }) + + const siwtchStatement = (isEnabled) => { + cy.get('[data-e2e=statement_setting]') + .click() + .then(() => { + cy.get('.ant-drawer-content').should('exist') + cy.get('[data-e2e=statemen_enbale_switcher]') + // the current of switcher is isEnabled + .should('have.attr', 'aria-checked', isEnabled) + .click() + cy.get('[data-e2e=submit_btn]').click() + }) + } + + it('Disable statement feature', () => { + siwtchStatement('true') + cy.get('.ant-modal-confirm-btns').find('.ant-btn-dangerous').click() + cy.get('[data-e2e=statements_table]').should('not.exist') + }) + + it('Enable statement feature', () => { + siwtchStatement('false') + cy.get('[data-e2e=statements_table]').should('exist') + }) + + describe('Default statement setting', () => { + beforeEach(() => { + cy.intercept(`${Cypress.env('apiBasePath')}statements/config`).as( + 'statements_config' + ) + + cy.get('[data-e2e=statement_setting]').click() + + // get refresh_interval value + cy.get(`[data-e2e=statement_setting_refresh_interval]`).within(() => { + cy.get('.ant-slider-handle') + .invoke('attr', 'aria-valuenow') + .as('refreshIntervalVal') + }) + + // get history_size value + cy.get(`[data-e2e=statement_setting_history_size]`).within(() => { + cy.get('.ant-slider-handle') + .invoke('attr', 'aria-valuenow') + .as('historySizeVal') + }) + }) + + const checkSilder = (sizeList, defaultValueNow, dataE2EValue) => { + cy.wait('@statements_config').then(() => { + cy.get(`[data-e2e=${dataE2EValue}]`).within(() => { + cy.get('.ant-slider-handle').should( + 'have.attr', + 'aria-valuenow', + defaultValueNow + ) + + cy.get('.ant-slider-mark-text') + .then(($marks) => { + return Cypress.$.makeArray($marks).map((mark) => mark.innerText) + }) + // make sure there exists the default executed statements + .should('to.deep.eq', sizeList) + }) + }) + } + + it('Default statement setting max size', () => { + const sizeList = ['200', '1000', '2000', '5000'] + const defaultMaxSizeValue = + Cypress.env('TIDB_VERSION') === 'v5.0.0' ? '200' : '3000' + checkSilder(sizeList, defaultMaxSizeValue, 'statement_setting_max_size') + }) + + it('Default statement setting window size', () => { + const sizeList = ['1', '5', '15', '30', '60'] + checkSilder(sizeList, '30', 'statement_setting_refresh_interval') + }) + + it('Default Statement setting number of windows', () => { + const sizeList = ['1', '255'] + checkSilder(sizeList, '24', 'statement_setting_history_size') + }) + + it('Default Check History Size', function () { + const stmtHistorySize = calcStmtHistorySize( + this.refreshIntervalVal, + this.historySizeVal + ) + cy.get('[data-e2e=statement_setting_keep_duration]').within(() => { + cy.get('.ant-form-item-control-input-content').should( + 'have.text', + stmtHistorySize + ) + }) + }) + }) + + describe('Update statement setting', function () { + beforeEach(function () { + cy.get('[data-e2e=statement_setting]').click() + }) + + it('Update window size and number of windows', function () { + // change window size + cy.get('[data-e2e=statement_setting_refresh_interval]').within(() => { + cy.get('.ant-slider-step') + .find('.ant-slider-dot') + .eq(2) + .click() + .then(() => { + cy.get('.ant-slider-handle') + .invoke('attr', 'aria-valuenow') + .as('refreshIntervalVal') + }) + }) + + // change number of windows + cy.get('[data-e2e=statement_setting_history_size]').within(() => { + cy.get('.ant-slider-step') + .find('.ant-slider-dot') + .eq(1) + .click() + .then(() => { + cy.get('.ant-slider-handle') + .invoke('attr', 'aria-valuenow') + .as('historySizeVal') + }) + }) + + cy.get('@refreshIntervalVal').then((refreshIntervalVal) => { + cy.get('@historySizeVal').then((historySizeVal) => { + cy.get('[data-e2e=statement_setting_keep_duration]').within(() => { + // check statement history size by calculating window size and # windows + const stmtHistorySize = calcStmtHistorySize( + refreshIntervalVal, + historySizeVal + ) + cy.get('.ant-form-item-control-input-content').should( + 'have.text', + stmtHistorySize + ) + }) + + cy.intercept( + 'POST', + `${Cypress.env('apiBasePath')}statements/config` + ).as('update_config') + cy.get('[data-e2e=submit_btn]').click() + + cy.wait('@update_config').then(() => { + // check configuration whether come to effect or not + cy.visit(this.uri.configuration) + cy.url().should('include', this.uri.configuration) + + cy.get('[data-e2e=search_config]').type( + 'tidb_stmt_summary_refresh_interval' + ) + cy.wait(1000) + cy.get('[data-automation-key=key]').contains( + 'tidb_stmt_summary_refresh_interval' + ) + cy.get('[data-automation-key=value]').contains( + refreshIntervalVal * 60 + ) + + cy.get('[data-e2e=search_config]') + .clear() + .type('tidb_stmt_summary_history_size') + cy.wait(1000) + cy.get('[data-automation-key=key]').contains( + 'tidb_stmt_summary_history_size' + ) + cy.get('[data-automation-key=value]').contains(historySizeVal) + }) + }) + }) + }) + + it('Failed to save config list', () => { + const staticResponse = { + statusCode: 400, + body: { + code: 'common.bad_request', + error: true, + message: 'common.bad_request', + }, + } + + // stub out a response body + cy.intercept( + 'POST', + `${Cypress.env('apiBasePath')}statements/config`, + staticResponse + ).as('statements_config') + cy.get('[data-e2e=submit_btn]').click() + cy.wait('@statements_config').then(() => { + // get error notifitcation on modal + cy.get('.ant-modal-confirm-content').should( + 'has.text', + staticResponse.body.message + ) + }) + }) + }) + }) + + describe('Simulate bad request', () => { + beforeEach(() => { + const staticResponse = { + statusCode: 400, + body: { + code: 'common.bad_request', + error: true, + message: 'common.bad_request', + }, + } + + // stub out a response body + cy.intercept( + `${Cypress.env('apiBasePath')}statements/config`, + staticResponse + ).as('failed_to_get_statements_config') + + cy.get('[data-e2e=statement_setting]').click() + }) + + it('Get config list bad request', () => { + cy.wait('@failed_to_get_statements_config').then(() => { + // get error alert on panel + cy.get('.ant-drawer-body').within(() => { + cy.get('[data-e2e=alert_error_bar]').should( + 'has.text', + 'common.bad_request' + ) + }) + }) + }) + }) + + describe('Export statement CSV ', () => { + it('validate CSV File', () => { + const downloadsFolder = Cypress.config('downloadsFolder') + let downloadedFilename + + cy.get('[data-e2e=statement_export_menu]') + .trigger('mouseover') + .then(() => { + cy.window() + .document() + .then(function (doc) { + doc.addEventListener('click', () => { + setTimeout(function () { + doc.location.reload() + }, 5000) + }) + + // Make sure the file exists + cy.intercept( + `${Cypress.env('apiBasePath')}statements/download?token=*` + ).as('download_statement') + + cy.get('[data-e2e=statement_export_btn]').click() + }) + }) + .then(() => { + cy.wait('@download_statement').then((res) => { + // join downloadFolder with CSV filename + const filenameRegx = /"(.*)"/ + downloadedFilename = path.join( + downloadsFolder, + res.response.headers['content-disposition'].match(filenameRegx)[1] + ) + + cy.readFile(downloadedFilename, { timeout: 15000 }) + // parse CSV text into objects + .then(neatCSV) + .then(validateStatementCSVList) + }) + }) + }) + }) +}) diff --git a/ui/cypress/integration/statement/02-detail.spec.js b/ui/cypress/integration/statement/02-detail.spec.js new file mode 100644 index 0000000000..3d48a355ba --- /dev/null +++ b/ui/cypress/integration/statement/02-detail.spec.js @@ -0,0 +1,202 @@ +describe('Statement detail page E2E test', () => { + before(() => { + const workloads = [ + 'DROP TABLE IF EXISTS mysql.t;', + 'CREATE TABLE `t` (`a` bigint(20) DEFAULT NULL, `b` bigint(20) DEFAULT NULL, `c` timestamp(6) DEFAULT CURRENT_TIMESTAMP(6), `d` varchar(50) DEFAULT NULL, UNIQUE KEY `idx0` (`a`), KEY `idx1` (`b`), KEY `idx2` (`b`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;', + 'select /*+ USE_INDEX(t, idx1) */ count(*) from t where b < 100;', + 'select /*+ USE_INDEX(t, idx2) */ count(*) from t where b < 100;', + ] + + workloads.forEach((query) => { + cy.task('queryDB', { query }) + }) + + cy.fixture('uri.json').then(function (uri) { + this.uri = uri + }) + }) + + beforeEach(function () { + cy.login('root') + cy.visit(this.uri.statement) + cy.url().should('include', this.uri.statement) + + cy.intercept( + `${Cypress.env('apiBasePath')}statements/plans?begin_time=*` + ).as('statements_plans') + + cy.intercept(`${Cypress.env('apiBasePath')}statements/plan/detail?*`).as( + 'statements_plan_detail' + ) + + cy.get('[data-automation-key=plan_count]').contains(2).eq(0).click() + }) + + describe('Statement Template', () => { + it('Check sql and default format', () => { + // sql is collapsed by default + cy.get('[data-e2e=expandText]').eq(0).should('have.text', 'Expand') + cy.get('[data-e2e=statement_query_detail_page_query]') + .eq(0) + .find('[data-e2e=syntax_highlighter_compact]') + .and('have.text', 'SELECT count (?) FROM `t` WHERE `b` < ?;') + }) + + it('Expand sql', () => { + // expand sql + cy.get('[data-e2e=expandText]').eq(0).click() + + // sql is collapsed by default + cy.get('[data-e2e=collapseText]').eq(0).should('have.text', 'Collapse') + cy.get('[data-e2e=statement_query_detail_page_query]') + .eq(0) + .find('[data-e2e=syntax_highlighter_original]') + .and('have.text', 'SELECT\n count (?)\nFROM\n `t`\nWHERE\n `b` < ?;') + }) + + it('Copy formatted sql to clipboard', () => { + cy.window().then((win) => { + cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') + }) + + cy.get('[data-e2e=copy_formatted_sql_to_clipboard]') + .realClick() + .then(() => { + cy.task('getClipboard').should( + 'eq', + 'SELECT\n count (?)\nFROM\n `t`\nWHERE\n `b` < ?;' + ) + }) + + cy.get('[data-e2e=copied_success]').should('exist') + }) + + it('Copy original sql to clipboard', () => { + cy.window().then((win) => { + cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') + }) + + cy.get('[data-e2e=copy_original_sql_to_clipboard]') + .realClick() + .then(() => { + cy.task('getClipboard').should( + 'eq', + 'select count ( ? ) from `t` where `b` < ? ;' + ) + }) + + cy.get('[data-e2e=copied_success]').should('exist') + }) + }) + + describe('Query Template', () => { + it('Check sql and default format', () => { + cy.wait('@statements_plan_detail').then((res) => { + const response = res.response.body + cy.get('.ant-descriptions-row') + .eq(3) + .within(() => { + cy.get('.ant-descriptions-item') + .eq(0) + .and('have.text', response.digest) + }) + }) + }) + }) + + describe('Plans', () => { + it('Has multiple execution plans', () => { + cy.wait('@statements_plans').then((res) => { + const response = res.response.body + const plansDigest = [] + + response.forEach((plan) => plansDigest.push(plan.plan_digest)) + + cy.get('[data-e2e=statement_multiple_execution_plans]') + .should('be.visible') + .within(() => { + // check digest of each plan + cy.get('[data-automation-key=plan_digest]') + .should('have.length', 2) + .then(($plans) => { + return Cypress.$.makeArray($plans).map((plan) => plan.innerText) + }) + .should('to.deep.equal', plansDigest) + + // all plans are checked + cy.get('.ms-DetailsList-headerWrapper').within(() => { + cy.get('.ant-checkbox').should( + 'have.class', + 'ant-checkbox-checked' + ) + }) + }) + }) + }) + }) + + describe('Detail tabs', () => { + it('Check tabs list', () => { + const tabList = [ + 'Basic', + 'Time', + 'Coprocessor Read', + 'Transaction', + 'Slow Query', + ] + cy.get('[data-e2e=tabs]') + .find('.ant-tabs-tab') + .should('have.length', 5) + .each(($tab, index) => { + cy.wrap($tab).should('have.text', tabList[index]) + }) + }) + }) + + describe('Detail table tabs', () => { + it('Basic table rows count', () => { + cy.wait('@statements_plan_detail').then(() => { + cy.get('[data-e2e=statement_pages_detail_tabs_basic]').within(() => { + cy.get('.ms-List-cell').should('have.length', 13) + }) + }) + }) + + it('Time table rows count', () => { + cy.get('.ant-tabs-tab').eq(1).click() + + cy.wait('@statements_plan_detail').then(() => { + cy.get('[data-e2e=statement_pages_detail_tabs_time]').within(() => { + cy.get('.ms-List-cell').should('have.length', 12) + }) + }) + }) + + it('Coprocessor table rows count', () => { + cy.get('.ant-tabs-tab').eq(2).click() + cy.wait('@statements_plan_detail').then(() => { + cy.get('[data-e2e=statement_pages_detail_tabs_copr]').within(() => { + cy.get('.ms-List-cell').should('have.length', 15) + }) + }) + }) + + it('Transaction table rows count', () => { + cy.get('.ant-tabs-tab').eq(3).click() + cy.wait('@statements_plan_detail').then(() => { + cy.get('[data-e2e=statement_pages_detail_tabs_txn]').within(() => { + cy.get('.ms-List-cell').should('have.length', 10) + }) + }) + }) + + it('Slow query table rows count', () => { + cy.get('.ant-tabs-tab').eq(4).click() + cy.wait('@statements_plan_detail').then(() => { + cy.get('[data-e2e=detail_tabs_slow_query]').within(() => { + cy.get('.ms-List-cell').should('have.length', 0) + }) + }) + }) + }) +}) diff --git a/ui/cypress/integration/statement/list.compat_spec.js b/ui/cypress/integration/statement/list.compat_spec.js new file mode 100644 index 0000000000..5969dc2b7c --- /dev/null +++ b/ui/cypress/integration/statement/list.compat_spec.js @@ -0,0 +1,63 @@ +// Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0. + +import { skipOn } from '@cypress/skip-test' + +describe('Read-only user open statement config setting', () => { + skipOn(Cypress.env('FEATURE_VERSION') !== '6.0.0', () => { + // Create read only user + before(() => { + const workloads = [ + 'DROP USER IF EXISTS "readOnlyUser"@"%"', + 'CREATE USER "readOnlyUser"@"%" IDENTIFIED BY "test";', + 'GRANT PROCESS, CONFIG ON *.* TO "readOnlyUser"@"%";', + 'GRANT SHOW DATABASES ON *.* TO "readOnlyUser"@"%";', + 'GRANT DASHBOARD_CLIENT ON *.* TO "readOnlyUser"@"%";', + ] + + workloads.forEach((query) => { + cy.task('queryDB', { query }) + }) + + cy.fixture('uri.json').then(function (uri) { + this.uri = uri + }) + }) + + beforeEach(function () { + // login with readOnlyUser + cy.visit(this.uri.login) + cy.get('[data-e2e=signin_username_input]').clear().type('readOnlyUser') + cy.get('[data-e2e="signin_password_input"]').type('test{enter}') + + cy.visit(this.uri.statement) + cy.url().should('include', this.uri.statement) + }) + + it('Unable to modify statement settings', function () { + cy.get('[data-e2e=statement_setting]').click({ force: true }) + + // switch is disabled + cy.get('[data-e2e=statemen_enbale_switcher]').should( + 'have.class', + 'ant-switch-disabled' + ) + + // max size is disabled + cy.get('[data-e2e=statement_setting_max_size]').within(() => { + cy.get('.ant-slider').should('have.class', 'ant-slider-disabled') + }) + + // refresh interval is disabled + cy.get('[data-e2e=statement_setting_refresh_interval]').within(() => { + cy.get('.ant-slider').should('have.class', 'ant-slider-disabled') + }) + // internal query is disabled + cy.get('[data-e2e=statement_setting_internal_query]').within(() => { + cy.get('.ant-switch').should('have.class', 'ant-switch-disabled') + }) + + // save button is disableds + cy.get('[data-e2e=submit_btn]').should('have.attr', 'disabled') + }) + }) +}) diff --git a/ui/cypress/integration/utils.js b/ui/cypress/integration/utils.js index cbf0fadf6f..86366f9e32 100644 --- a/ui/cypress/integration/utils.js +++ b/ui/cypress/integration/utils.js @@ -1,3 +1,5 @@ +// Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0. + /** * Delete the downloads folder to make sure the test has "clean" * slate before starting. @@ -11,7 +13,7 @@ export const deleteDownloadsFolder = () => { /** * @param {string[]} list List parsed from CSV file */ -export const validateCSVList = (list) => { +export const validateSlowQueryCSVList = (list) => { expect(list).to.have.length(4) expect(list[0].query).to.equal('SELECT sleep(1.2);') @@ -19,3 +21,28 @@ export const validateCSVList = (list) => { expect(list[2].query).to.equal('SELECT sleep(2);') expect(list[3].query).to.equal('SELECT sleep(1);') } + +export const validateStatementCSVList = (allStatementList) => { + const defaultExecStmtList = [ + 'show databases', + 'select distinct `stmt_type` from `information_schema` . `cluster_statements_summary_history` order by `stmt_type` asc', + 'select `version` ( )', + ] + + const allStatementDigestText = [] + allStatementList.forEach((stmt) => { + allStatementDigestText.push(stmt.digest_text) + }) + expect(allStatementDigestText).to.include.members(defaultExecStmtList) +} + +export const restartTiUP = () => { + // Restart tiup + cy.exec( + `bash ../scripts/start_tiup.sh ${Cypress.env('TIDB_VERSION')} restart`, + { log: true } + ) + + // Wait TiUP Playground + cy.exec('bash ../scripts/wait_tiup_playground.sh 1 300 &> wait_tiup.log') +} diff --git a/ui/lib/apps/Configuration/index.tsx b/ui/lib/apps/Configuration/index.tsx index 5d6453a7f1..303e03086c 100644 --- a/ui/lib/apps/Configuration/index.tsx +++ b/ui/lib/apps/Configuration/index.tsx @@ -200,7 +200,11 @@ export default function () {
- +
diff --git a/ui/lib/apps/Overview/components/RecentStatements.tsx b/ui/lib/apps/Overview/components/RecentStatements.tsx index a39fd7cf5a..26507a5141 100644 --- a/ui/lib/apps/Overview/components/RecentStatements.tsx +++ b/ui/lib/apps/Overview/components/RecentStatements.tsx @@ -13,7 +13,6 @@ const visibleColumnKeys: IColumnKeys = { digest_text: true, sum_latency: true, avg_latency: true, - related_schemas: true, } export default function RecentStatements() { diff --git a/ui/lib/apps/SlowQuery/components/SlowQueriesTable.tsx b/ui/lib/apps/SlowQuery/components/SlowQueriesTable.tsx index 00cf0e9e21..9e819603bc 100644 --- a/ui/lib/apps/SlowQuery/components/SlowQueriesTable.tsx +++ b/ui/lib/apps/SlowQuery/components/SlowQueriesTable.tsx @@ -53,6 +53,7 @@ function SlowQueriesTable({ controller, ...restProps }: Props) { onRowClicked={handleRowClick} clickedRowIndex={getClickedItemIndex()} getKey={getKey} + data-e2e="detail_tabs_slow_query" /> ) } diff --git a/ui/lib/apps/SlowQuery/pages/List/index.tsx b/ui/lib/apps/SlowQuery/pages/List/index.tsx index f696eac7c5..977237d833 100644 --- a/ui/lib/apps/SlowQuery/pages/List/index.tsx +++ b/ui/lib/apps/SlowQuery/pages/List/index.tsx @@ -128,6 +128,7 @@ function List() { }) } items={allSchemas} + data-e2e="execution_database_name" /> ) }, @@ -52,6 +53,7 @@ export default function DetailTabs({ columns={columns} items={items} extendLastColumn + data-e2e="statement_pages_detail_tabs_time" /> ) }, @@ -70,6 +72,7 @@ export default function DetailTabs({ columns={columns} items={items} extendLastColumn + data-e2e="statement_pages_detail_tabs_copr" /> ) }, @@ -86,6 +89,7 @@ export default function DetailTabs({ columns={columns} items={items} extendLastColumn + data-e2e="statement_pages_detail_tabs_txn" /> ) }, diff --git a/ui/lib/apps/Statement/pages/Detail/index.tsx b/ui/lib/apps/Statement/pages/Detail/index.tsx index 484fa76f68..7045036eb6 100644 --- a/ui/lib/apps/Statement/pages/Detail/index.tsx +++ b/ui/lib/apps/Statement/pages/Detail/index.tsx @@ -168,6 +168,7 @@ function DetailPage() { style={{ display: plans && plans.length > 1 ? 'block' : 'none', }} + data-e2e="statement_multiple_execution_plans" > - + @@ -128,6 +132,7 @@ function StatementSettingForm({ onClose, onConfigUpdated }: Props) { @@ -144,6 +149,7 @@ function StatementSettingForm({ onClose, onConfigUpdated }: Props) { @@ -163,6 +169,7 @@ function StatementSettingForm({ onClose, onConfigUpdated }: Props) { prev.refresh_interval !== cur.refresh_interval || prev.history_size !== cur.history_size } + data-e2e="statement_setting_keep_duration" > {({ getFieldValue }) => { const refreshInterval = @@ -180,6 +187,7 @@ function StatementSettingForm({ onClose, onConfigUpdated }: Props) { extra={t('statement.settings.internal_query_tooltip')} name="internal_query" valuePropName="checked" + data-e2e="statement_setting_internal_query" > @@ -194,6 +202,7 @@ function StatementSettingForm({ onClose, onConfigUpdated }: Props) { htmlType="submit" loading={submitting} disabled={!isWriteable} + data-e2e="submit_btn" > {t('statement.settings.actions.save')} diff --git a/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx b/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx index a4bac9bae8..174b26df39 100644 --- a/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx +++ b/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx @@ -214,6 +214,7 @@ export default function TimeRangeSelector({ curTimeRange.value === seconds, })} onClick={() => enabled && handleRecentChange(seconds)} + data-e2e="statement_time_range_option" > {t('statement.pages.overview.toolbar.time_range_selector.recent')}{' '} {getValueFormat('s')(seconds, 0)} @@ -253,7 +254,10 @@ export default function TimeRangeSelector({ visible={dropdownVisible} onVisibleChange={setDropdownVisible} > - diff --git a/ui/lib/components/Expand/index.tsx b/ui/lib/components/Expand/index.tsx index 853af5ce85..65ac41c2bc 100644 --- a/ui/lib/components/Expand/index.tsx +++ b/ui/lib/components/Expand/index.tsx @@ -11,7 +11,7 @@ export interface IExpandProps { function Expand({ collapsedContent, children, expanded }: IExpandProps) { // FIXME: Animations return ( -
+
{expanded ? children : collapsedContent ?? children}
) diff --git a/ui/lib/utils/tableColumnFactory.tsx b/ui/lib/utils/tableColumnFactory.tsx index 81486003f9..803fd73cc0 100644 --- a/ui/lib/utils/tableColumnFactory.tsx +++ b/ui/lib/utils/tableColumnFactory.tsx @@ -235,7 +235,7 @@ export class TableColumnFactory { isMultiline: showFullSQL, onRender: (rec: U) => showFullSQL ? ( - + ) : ( @@ -243,7 +243,7 @@ export class TableColumnFactory { title={} placement="right" > - +