e2e.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. import '@cypress/code-coverage/support';
  20. import '@applitools/eyes-cypress/commands';
  21. import { expect } from 'chai';
  22. import rison from 'rison';
  23. /* eslint-disable @typescript-eslint/no-explicit-any */
  24. require('cy-verify-downloads').addCustomCommand();
  25. // fail on console error, allow config to override individual tests
  26. // these exceptions are a little pile of tech debt
  27. //
  28. // DISABLING FOR NOW
  29. /*
  30. const { getConfig, setConfig } = failOnConsoleError({
  31. consoleMessages: [
  32. /\[webpack-dev-server\]/,
  33. 'The pseudo class ":first-child" is potentially unsafe when doing server-side rendering. Try changing it to ":first-of-type".',
  34. 'The pseudo class ":nth-child" is potentially unsafe when doing server-side rendering. Try changing it to ":nth-of-type".',
  35. 'Error: Unknown Error',
  36. /Unable to infer path to ace from script src/,
  37. ],
  38. includeConsoleTypes: ['error'],
  39. });
  40. */
  41. // Set individual tests to allow certain console errors to NOT fail, e.g
  42. // cy.allowConsoleErrors(['foo', /^some bar-regex.*/]);
  43. // This will be reset between tests.
  44. Cypress.Commands.addAll({
  45. getConsoleMessages: () => cy.wrap(getConfig()?.consoleMessages),
  46. allowConsoleErrors: (consoleMessages: (string | RegExp)[]) =>
  47. setConfig({ ...getConfig(), consoleMessages }),
  48. });
  49. const BASE_EXPLORE_URL = '/explore/?form_data=';
  50. let DASHBOARD_FIXTURES: Record<string, any>[] = [];
  51. let CHART_FIXTURES: Record<string, any>[] = [];
  52. Cypress.Commands.add('loadChartFixtures', () =>
  53. cy.fixture('charts.json').then(charts => {
  54. CHART_FIXTURES = charts;
  55. }),
  56. );
  57. Cypress.Commands.add('loadDashboardFixtures', () =>
  58. cy.fixture('dashboards.json').then(dashboards => {
  59. DASHBOARD_FIXTURES = dashboards;
  60. }),
  61. );
  62. const PATHS_TO_SKIP_LOGIN = ['login', 'register'];
  63. const skipLogin = () => {
  64. for (const path of PATHS_TO_SKIP_LOGIN) {
  65. if (Cypress.currentTest.title.toLowerCase().includes(path)) {
  66. return true;
  67. }
  68. }
  69. return false;
  70. };
  71. before(() => {
  72. if (skipLogin()) {
  73. return;
  74. }
  75. cy.login();
  76. Cypress.Cookies.defaults({ preserve: 'session' });
  77. cy.loadChartFixtures();
  78. cy.loadDashboardFixtures();
  79. });
  80. beforeEach(() => {
  81. if (skipLogin()) {
  82. return;
  83. }
  84. cy.cleanDashboards();
  85. cy.cleanCharts();
  86. });
  87. Cypress.Commands.add('cleanDashboards', () => {
  88. cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
  89. const deletableDashboards = [];
  90. for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
  91. const fixture = DASHBOARD_FIXTURES[i];
  92. const isInDb = sampleDashboards?.find(
  93. d => d.dashboard_title === fixture.dashboard_title,
  94. );
  95. if (isInDb) {
  96. deletableDashboards.push(isInDb.id);
  97. }
  98. }
  99. if (deletableDashboards.length) {
  100. cy.request({
  101. failOnStatusCode: false,
  102. method: 'DELETE',
  103. url: `api/v1/dashboard/?q=!(${deletableDashboards.join(',')})`,
  104. headers: {
  105. Cookie: `csrf_access_token=${window.localStorage.getItem(
  106. 'access_token',
  107. )}`,
  108. 'Content-Type': 'application/json',
  109. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  110. Referer: `${Cypress.config().baseUrl}/`,
  111. },
  112. }).then(resp => resp);
  113. }
  114. });
  115. });
  116. Cypress.Commands.add('cleanCharts', () => {
  117. cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
  118. const deletableCharts = [];
  119. for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
  120. const fixture = CHART_FIXTURES[i];
  121. const isInDb = sampleCharts?.find(
  122. c => c.slice_name === fixture.slice_name,
  123. );
  124. if (isInDb) {
  125. deletableCharts.push(isInDb.id);
  126. }
  127. }
  128. if (deletableCharts.length) {
  129. cy.request({
  130. failOnStatusCode: false,
  131. method: 'DELETE',
  132. url: `api/v1/chart/?q=!(${deletableCharts.join(',')})`,
  133. headers: {
  134. Cookie: `csrf_access_token=${window.localStorage.getItem(
  135. 'access_token',
  136. )}`,
  137. 'Content-Type': 'application/json',
  138. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  139. Referer: `${Cypress.config().baseUrl}/`,
  140. },
  141. }).then(resp => resp);
  142. }
  143. });
  144. });
  145. Cypress.Commands.add('getBySel', (selector, ...args) =>
  146. cy.get(`[data-test=${selector}]`, ...args),
  147. );
  148. Cypress.Commands.add('getBySelLike', (selector, ...args) =>
  149. cy.get(`[data-test*=${selector}]`, ...args),
  150. );
  151. /* eslint-disable consistent-return */
  152. Cypress.on('uncaught:exception', err => {
  153. // ignore ResizeObserver client errors, as they are unrelated to operation
  154. // and causing flaky test failures in CI
  155. if (err.message && /ResizeObserver loop limit exceeded/.test(err.message)) {
  156. // returning false here prevents Cypress from failing the test
  157. return false;
  158. }
  159. return false; // TODO:@geido remove
  160. });
  161. /* eslint-enable consistent-return */
  162. Cypress.Commands.add('login', () => {
  163. cy.request({
  164. method: 'POST',
  165. url: '/login/',
  166. body: { username: 'admin', password: 'general' },
  167. }).then(response => {
  168. if (response.status === 302) {
  169. // If there's a redirect, follow it manually
  170. const redirectUrl = response.headers.location;
  171. cy.request({
  172. method: 'GET',
  173. url: redirectUrl,
  174. }).then(finalResponse => {
  175. expect(finalResponse.status).to.eq(200);
  176. });
  177. } else {
  178. expect(response.status).to.eq(200);
  179. }
  180. });
  181. });
  182. Cypress.Commands.add('visitChartByName', name => {
  183. const query = rison.encode({
  184. columns: ['id'],
  185. filters: [{ col: 'slice_name', opr: 'eq', value: name }],
  186. });
  187. cy.request(`/api/v1/chart?q=${query}`).then(response => {
  188. cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.result[0].id}}`);
  189. });
  190. });
  191. Cypress.Commands.add('visitChartById', chartId =>
  192. cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
  193. );
  194. Cypress.Commands.add(
  195. 'visitChartByParams',
  196. (formData: {
  197. datasource?: string;
  198. datasource_id?: number;
  199. datasource_type?: string;
  200. [key: string]: unknown;
  201. }) => {
  202. let datasource_id;
  203. let datasource_type;
  204. if (formData.datasource_id && formData.datasource_type) {
  205. ({ datasource_id, datasource_type } = formData);
  206. } else {
  207. [datasource_id, datasource_type] = formData.datasource?.split('__') || [];
  208. }
  209. const accessToken = window.localStorage.getItem('access_token');
  210. cy.request({
  211. method: 'POST',
  212. url: 'api/v1/explore/form_data',
  213. body: {
  214. datasource_id,
  215. datasource_type,
  216. form_data: JSON.stringify(formData),
  217. },
  218. headers: {
  219. ...(accessToken && {
  220. Cookie: `csrf_access_token=${accessToken}`,
  221. 'X-CSRFToken': accessToken,
  222. }),
  223. 'Content-Type': 'application/json',
  224. Referer: `${Cypress.config().baseUrl}/`,
  225. },
  226. }).then(response => {
  227. const formDataKey = response.body.key;
  228. const url = `/explore/?form_data_key=${formDataKey}`;
  229. cy.visit(url);
  230. });
  231. },
  232. );
  233. Cypress.Commands.add('verifySliceContainer', chartSelector => {
  234. // After a wait response check for valid slice container
  235. cy.get('.slice_container')
  236. .should('be.visible')
  237. .within(() => {
  238. if (chartSelector) {
  239. cy.get(chartSelector)
  240. .should('be.visible')
  241. .then(chart => {
  242. expect(chart[0].clientWidth).greaterThan(0);
  243. expect(chart[0].clientHeight).greaterThan(0);
  244. });
  245. }
  246. });
  247. return cy;
  248. });
  249. Cypress.Commands.add(
  250. 'verifySliceSuccess',
  251. ({
  252. waitAlias,
  253. querySubstring,
  254. chartSelector,
  255. }: {
  256. waitAlias: string;
  257. chartSelector: JQuery.Selector;
  258. querySubstring?: string | RegExp;
  259. }) => {
  260. cy.wait(waitAlias).then(({ response }) => {
  261. cy.verifySliceContainer(chartSelector);
  262. const responseBody = response?.body;
  263. if (querySubstring) {
  264. const query: string =
  265. responseBody.query || responseBody.result[0].query || '';
  266. if (querySubstring instanceof RegExp) {
  267. expect(query).to.match(querySubstring);
  268. } else {
  269. expect(query).to.contain(querySubstring);
  270. }
  271. }
  272. });
  273. return cy;
  274. },
  275. );
  276. Cypress.Commands.add('createSampleDashboards', (indexes?: number[]) =>
  277. cy.cleanDashboards().then(() => {
  278. for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
  279. if (indexes?.includes(i) || !indexes) {
  280. cy.request({
  281. method: 'POST',
  282. url: `/api/v1/dashboard/`,
  283. body: DASHBOARD_FIXTURES[i],
  284. failOnStatusCode: false,
  285. headers: {
  286. Cookie: `csrf_access_token=${window.localStorage.getItem(
  287. 'access_token',
  288. )}`,
  289. 'Content-Type': 'application/json',
  290. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  291. Referer: `${Cypress.config().baseUrl}/`,
  292. },
  293. });
  294. }
  295. }
  296. }),
  297. );
  298. Cypress.Commands.add('createSampleCharts', (indexes?: number[]) =>
  299. cy.cleanCharts().then(() => {
  300. for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
  301. if (indexes?.includes(i) || !indexes) {
  302. cy.request({
  303. method: 'POST',
  304. url: `/api/v1/chart/`,
  305. body: CHART_FIXTURES[i],
  306. failOnStatusCode: false,
  307. headers: {
  308. Cookie: `csrf_access_token=${window.localStorage.getItem(
  309. 'access_token',
  310. )}`,
  311. 'Content-Type': 'application/json',
  312. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  313. Referer: `${Cypress.config().baseUrl}/`,
  314. },
  315. });
  316. }
  317. }
  318. }),
  319. );
  320. Cypress.Commands.add(
  321. 'deleteDashboardByName',
  322. (dashboardName: string, failOnStatusCode = false) =>
  323. cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
  324. const dashboard = sampleDashboards?.find(
  325. d => d.dashboard_title === dashboardName,
  326. );
  327. if (dashboard) {
  328. cy.deleteDashboard(dashboard.id, failOnStatusCode);
  329. }
  330. }),
  331. );
  332. Cypress.Commands.add(
  333. 'deleteDashboard',
  334. (id: number, failOnStatusCode = false) =>
  335. cy
  336. .request({
  337. failOnStatusCode,
  338. method: 'DELETE',
  339. url: `api/v1/dashboard/${id}`,
  340. headers: {
  341. Cookie: `csrf_access_token=${window.localStorage.getItem(
  342. 'access_token',
  343. )}`,
  344. 'Content-Type': 'application/json',
  345. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  346. Referer: `${Cypress.config().baseUrl}/`,
  347. },
  348. })
  349. .then(resp => resp),
  350. );
  351. Cypress.Commands.add('getDashboards', () => {
  352. cy.request({
  353. method: 'GET',
  354. url: `api/v1/dashboard/`,
  355. headers: {
  356. 'Content-Type': 'application/json',
  357. },
  358. }).then(resp => resp.body.result);
  359. });
  360. Cypress.Commands.add('getDashboard', (dashboardId: string | number) =>
  361. cy
  362. .request({
  363. method: 'GET',
  364. url: `api/v1/dashboard/${dashboardId}`,
  365. headers: {
  366. 'Content-Type': 'application/json',
  367. },
  368. })
  369. .then(resp => resp.body.result),
  370. );
  371. Cypress.Commands.add(
  372. 'updateDashboard',
  373. (dashboardId: number, body: Record<string, any>) =>
  374. cy
  375. .request({
  376. method: 'PUT',
  377. url: `api/v1/dashboard/${dashboardId}`,
  378. body,
  379. headers: {
  380. 'Content-Type': 'application/json',
  381. },
  382. })
  383. .then(resp => resp.body.result),
  384. );
  385. Cypress.Commands.add('deleteChart', (id: number, failOnStatusCode = false) =>
  386. cy
  387. .request({
  388. failOnStatusCode,
  389. method: 'DELETE',
  390. url: `api/v1/chart/${id}`,
  391. headers: {
  392. Cookie: `csrf_access_token=${window.localStorage.getItem(
  393. 'access_token',
  394. )}`,
  395. 'Content-Type': 'application/json',
  396. 'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
  397. Referer: `${Cypress.config().baseUrl}/`,
  398. },
  399. })
  400. .then(resp => resp),
  401. );
  402. Cypress.Commands.add('getCharts', () =>
  403. cy
  404. .request({
  405. method: 'GET',
  406. url: `api/v1/chart/`,
  407. headers: {
  408. 'Content-Type': 'application/json',
  409. },
  410. })
  411. .then(resp => resp.body.result),
  412. );
  413. Cypress.Commands.add(
  414. 'deleteChartByName',
  415. (sliceName: string, failOnStatusCode = false) =>
  416. cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
  417. const chart = sampleCharts?.find(c => c.slice_name === sliceName);
  418. if (chart) {
  419. cy.deleteChart(chart.id, failOnStatusCode);
  420. }
  421. }),
  422. );