mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana packages: Remove E2E workspace (#86416)
* remove e2e package code and any code referencing it * update code owners * remove more references to e2e package * remove unrelated file
This commit is contained in:
parent
f1aa6549f6
commit
a3ef463499
@ -6353,9 +6353,6 @@ exports[`no gf-form usage`] = {
|
||||
"e2e/utils/flows/addDataSource.ts:5381": [
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
"packages/grafana-e2e/src/flows/addDataSource.ts:5381": [
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/components/PromExploreExtraField.tsx:5381": [
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
|
@ -7005,10 +7005,6 @@
|
||||
"path": "/e2e/utils/flows/addDataSource.ts",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"path": "/packages/grafana-e2e/src/flows/addDataSource.ts",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"path": "/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx",
|
||||
"count": 4
|
||||
|
@ -7,7 +7,6 @@ import { glob } from 'glob';
|
||||
// Why are we ignoring these?
|
||||
// They're all deprecated/being removed so doesn't make sense to fix types
|
||||
const eslintPathsToIgnore = [
|
||||
'packages/grafana-e2e', // deprecated.
|
||||
'public/app/angular', // will be removed in Grafana 11
|
||||
'public/app/plugins/panel/graph', // will be removed alongside angular
|
||||
'public/app/plugins/panel/table-old', // will be removed alongside angular
|
||||
|
68
.eslintrc
68
.eslintrc
@ -4,7 +4,7 @@
|
||||
"plugins": ["@emotion", "lodash", "jest", "import", "jsx-a11y", "@grafana", "no-barrel-files"],
|
||||
"settings": {
|
||||
"import/internal-regex": "^(app/)|(@grafana)",
|
||||
"import/external-module-folders": ["node_modules", ".yarn"]
|
||||
"import/external-module-folders": ["node_modules", ".yarn"],
|
||||
},
|
||||
"rules": {
|
||||
"@grafana/no-border-radius-literal": "error",
|
||||
@ -19,8 +19,8 @@
|
||||
{
|
||||
"groups": [["builtin", "external"], "internal", "parent", "sibling", "index"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": { "order": "asc" }
|
||||
}
|
||||
"alphabetize": { "order": "asc" },
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
@ -29,47 +29,43 @@
|
||||
{
|
||||
"name": "react-redux",
|
||||
"importNames": ["useDispatch", "useSelector"],
|
||||
"message": "Please import from app/types instead."
|
||||
"message": "Please import from app/types instead.",
|
||||
},
|
||||
{
|
||||
"name": "react-i18next",
|
||||
"importNames": ["Trans", "t"],
|
||||
"message": "Please import from app/core/internationalization instead"
|
||||
"message": "Please import from app/core/internationalization instead",
|
||||
},
|
||||
{
|
||||
"name": "@grafana/e2e",
|
||||
"message": "@grafana/e2e is deprecated. Please import from ./e2e/utils instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// Use typescript's no-redeclare for compatibility with overrides
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": ["error"]
|
||||
"@typescript-eslint/no-redeclare": ["error"],
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["packages/grafana-ui/src/components/uPlot/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["packages/grafana-ui/src/components/ThemeDemos/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"@emotion/jsx-import": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["public/dashboards/scripted*.js"],
|
||||
"rules": {
|
||||
"no-redeclare": "error",
|
||||
"@typescript-eslint/no-redeclare": "off"
|
||||
}
|
||||
"@typescript-eslint/no-redeclare": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"extends": ["plugin:jsx-a11y/recommended"],
|
||||
@ -82,17 +78,17 @@
|
||||
"jsx-a11y/no-autofocus": [
|
||||
"error",
|
||||
{
|
||||
"ignoreNonDOM": true
|
||||
}
|
||||
"ignoreNonDOM": true,
|
||||
},
|
||||
],
|
||||
"jsx-a11y/label-has-associated-control": [
|
||||
"error",
|
||||
{
|
||||
"controlComponents": ["NumberInput"],
|
||||
"depth": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
"depth": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
@ -125,14 +121,14 @@
|
||||
"public/app/plugins/datasource/cloudwatch/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/cloudwatch/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/zipkin/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/zipkin/**/*.{ts,tsx}"
|
||||
"public/app/plugins/datasource/zipkin/**/*.{ts,tsx}",
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
"extensions": [".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"import/no-restricted-paths": [
|
||||
@ -143,12 +139,12 @@
|
||||
"target": "./public/app/plugins",
|
||||
"from": "./public",
|
||||
"except": ["./app/plugins"],
|
||||
"message": "Core plugins are not allowed to depend on Grafana core packages"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
"message": "Core plugins are not allowed to depend on Grafana core packages",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -313,7 +313,6 @@
|
||||
/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
|
||||
/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||
/packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-e2e/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-ui/.storybook/ @grafana/plugins-platform-frontend
|
||||
/packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform
|
||||
|
2
.github/renovate.json5
vendored
2
.github/renovate.json5
vendored
@ -13,7 +13,7 @@
|
||||
"@swc/core", // versions ~1.4.5 contain multiple bugs related to baseUrl resolution breaking builds.
|
||||
],
|
||||
"includePaths": ["package.json", "packages/**", "public/app/plugins/**"],
|
||||
"ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**", "packages/grafana-e2e/**"],
|
||||
"ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**"],
|
||||
"labels": ["area/frontend", "dependencies", "no-changelog"],
|
||||
"postUpdateOptions": ["yarnDedupeHighest"],
|
||||
"packageRules": [
|
||||
|
@ -10,7 +10,6 @@ The following directories and their subdirectories are licensed under Apache-2.0
|
||||
|
||||
```
|
||||
packages/grafana-data/
|
||||
packages/grafana-e2e/
|
||||
packages/grafana-e2e-selectors/
|
||||
packages/grafana-runtime/
|
||||
packages/grafana-ui/
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error", { "patterns": ["@grafana/runtime", "@grafana/ui", "@grafana/data", "@grafana/e2e/*"] }]
|
||||
"no-restricted-imports": ["error", { "patterns": ["@grafana/runtime", "@grafana/ui", "@grafana/data"] }],
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.{ts,tsx}"],
|
||||
"rules": {
|
||||
"no-restricted-imports": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
"no-restricted-imports": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
3
packages/grafana-e2e/.gitignore
vendored
3
packages/grafana-e2e/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
test/cypress/report.json
|
||||
test/cypress/screenshots/actual
|
||||
test/cypress/videos/
|
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Grafana Labs
|
||||
|
||||
Licensed 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.
|
@ -1,5 +0,0 @@
|
||||
# Grafana End-to-End Test library
|
||||
|
||||
> [!CAUTION]
|
||||
> This package is deprecated.
|
||||
> If you'd like to write end-to-end tests for a Grafana plugin (core or external), use the [`@grafana/plugin-e2e`](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/introduction) package.
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../cli')();
|
@ -1,57 +0,0 @@
|
||||
const { program } = require('commander');
|
||||
const execa = require('execa');
|
||||
const { resolve, sep } = require('path');
|
||||
const resolveBin = require('resolve-bin');
|
||||
|
||||
const cypress = (commandName, { updateScreenshots, browser }) => {
|
||||
// Support running an unpublished dev build
|
||||
const dirname = __dirname.split(sep).pop();
|
||||
const projectPath = resolve(`${__dirname}${dirname === 'dist' ? '/..' : ''}`);
|
||||
|
||||
// For plugins/extendConfig
|
||||
const CWD = `CWD=${process.cwd()}`;
|
||||
|
||||
// For plugins/compareSnapshots
|
||||
const UPDATE_SCREENSHOTS = `UPDATE_SCREENSHOTS=${updateScreenshots ? 1 : 0}`;
|
||||
|
||||
const cypressOptions = [commandName, '--env', `${CWD},${UPDATE_SCREENSHOTS}`, `--project=${projectPath}`];
|
||||
|
||||
if (browser) {
|
||||
cypressOptions.push('--browser', browser);
|
||||
}
|
||||
|
||||
const execaOptions = {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit',
|
||||
};
|
||||
|
||||
return execa(resolveBin.sync('cypress'), cypressOptions, execaOptions)
|
||||
.then(() => {}) // no return value
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
const updateOption = '-u, --update-screenshots';
|
||||
const updateDescription = 'update expected screenshots';
|
||||
const browserOption = '-b, --browser <browser>';
|
||||
const browserDescription = 'specify which browser to use';
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.description('runs tests within the interactive GUI')
|
||||
.option(updateOption, updateDescription)
|
||||
.option(browserOption, browserDescription)
|
||||
.action((options) => cypress('open', options));
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.description('runs tests from the CLI without the GUI')
|
||||
.option(updateOption, updateDescription)
|
||||
.option(browserOption, browserDescription)
|
||||
.action((options) => cypress('run', options));
|
||||
|
||||
program.parse(process.argv);
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"projectId": "zb7k1c",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videoCompression": 20,
|
||||
"viewportWidth": 1920,
|
||||
"viewportHeight": 1080
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
@ -1,323 +0,0 @@
|
||||
{
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))",
|
||||
"refId": "A",
|
||||
"meta": { "custom": { "resultType": "matrix" } },
|
||||
"fields": [
|
||||
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
|
||||
{
|
||||
"name": "Value",
|
||||
"type": "number",
|
||||
"typeInfo": { "frame": "float64" },
|
||||
"labels": {},
|
||||
"config": {
|
||||
"displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000,
|
||||
1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000,
|
||||
1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000
|
||||
],
|
||||
[
|
||||
0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913,
|
||||
0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543,
|
||||
0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052,
|
||||
0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964,
|
||||
0.07769657495236215, 0.077550401329267
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))",
|
||||
"refId": "A",
|
||||
"meta": { "custom": { "resultType": "vector" } },
|
||||
"fields": [
|
||||
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
|
||||
{
|
||||
"name": "Value",
|
||||
"type": "number",
|
||||
"typeInfo": { "frame": "float64" },
|
||||
"labels": {},
|
||||
"config": {
|
||||
"displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": { "values": [[1633619900000], [0.0775504013292671]] }
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"name": "exemplar",
|
||||
"refId": "A",
|
||||
"meta": { "custom": { "resultType": "exemplar" } },
|
||||
"fields": [
|
||||
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
|
||||
{ "name": "Value", "type": "number", "typeInfo": { "frame": "float64" } },
|
||||
{ "name": "instance", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "__name__", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "job", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "status_code", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "method", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "traceID", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "route", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "ws", "type": "string", "typeInfo": { "frame": "string" } },
|
||||
{ "name": "le", "type": "string", "typeInfo": { "frame": "string" } }
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000,
|
||||
1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000,
|
||||
1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000,
|
||||
1633619877000, 1633619889000
|
||||
],
|
||||
[
|
||||
0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026,
|
||||
0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967,
|
||||
0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284
|
||||
],
|
||||
[
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"db:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80",
|
||||
"app:80"
|
||||
],
|
||||
[
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket",
|
||||
"tns_request_duration_seconds_bucket"
|
||||
],
|
||||
[
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/db",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app",
|
||||
"tns/app"
|
||||
],
|
||||
[
|
||||
"302",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"500",
|
||||
"200",
|
||||
"302",
|
||||
"208",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"302",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200",
|
||||
"200"
|
||||
],
|
||||
[
|
||||
"POST",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"POST",
|
||||
"POST",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"POST",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET",
|
||||
"GET"
|
||||
],
|
||||
[
|
||||
"6a3cf561ef6c32a0",
|
||||
"396bcdf29601a149",
|
||||
"57c04ef608f11158",
|
||||
"77c757dab83c665f",
|
||||
"3d1069567e873f5e",
|
||||
"b337949f6213efd",
|
||||
"21b20cbe533cf099",
|
||||
"2c10b3aa30fabd66",
|
||||
"42ac6088a757636b",
|
||||
"2f81158008cd4dcc",
|
||||
"320b803ad7323b37",
|
||||
"7f15fd82aeb8b361",
|
||||
"11c79266da8a74cd",
|
||||
"5a8571bdcc04c990",
|
||||
"3de3f4f42ccb93ae",
|
||||
"23343ac91cc0638",
|
||||
"5cea3aad17ab11c8",
|
||||
"5d334e2843d3405a",
|
||||
"3cf6834596d4b6b6",
|
||||
"1ab6cff012959723",
|
||||
"2f78bc2c398b8b20",
|
||||
"6d5862a70c3abd42",
|
||||
"f5421be4054f501"
|
||||
],
|
||||
[
|
||||
"post",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"post",
|
||||
"post",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"post",
|
||||
"metrics",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root",
|
||||
"root"
|
||||
],
|
||||
[
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false"
|
||||
],
|
||||
[
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.05",
|
||||
"0.05",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.25",
|
||||
"0.1",
|
||||
"0.25",
|
||||
"0.05",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.05",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"1.0",
|
||||
"0.05",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.025",
|
||||
"0.025"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,81 +0,0 @@
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "matrix",
|
||||
"result": [
|
||||
{
|
||||
"metric": {},
|
||||
"values": [
|
||||
[1620758235, "0.07554431352019486"],
|
||||
[1620758250, "0.0756695553961457"],
|
||||
[1620758265, "0.0757369945411682"],
|
||||
[1620758280, "0.07560212035898113"],
|
||||
[1620758295, "0.07556358506832812"],
|
||||
[1620758310, "0.07558766859344893"],
|
||||
[1620758325, "0.07552022996976834"],
|
||||
[1620758340, "0.07553949807996531"],
|
||||
[1620758355, "0.07554913414209416"],
|
||||
[1620758370, "0.07539017545449077"],
|
||||
[1620758385, "0.07524566527721041"],
|
||||
[1620758400, "0.06631294924007665"],
|
||||
[1620758415, "0.020769530989205368"],
|
||||
[1620758430, "0.05720168751283235"],
|
||||
[1620758445, "0.07271760187022697"],
|
||||
[1620758460, "0.07282398348834057"],
|
||||
[1620758475, "0.07272243619599422"],
|
||||
[1620758490, "0.0727659581600079"],
|
||||
[1620758505, "0.07290135207155769"],
|
||||
[1620758520, "0.07293036876672591"],
|
||||
[1620758535, "0.0727901374111541"],
|
||||
[1620758550, "0.07272727333735175"],
|
||||
[1620758565, "0.07264506733699574"],
|
||||
[1620758580, "0.07272243607717656"],
|
||||
[1620758595, "0.0728288184987238"],
|
||||
[1620758610, "0.07298839709448537"],
|
||||
[1620758625, "0.07301257421338406"],
|
||||
[1620758640, "0.07304158515671498"],
|
||||
[1620758655, "0.07311895518980911"],
|
||||
[1620758670, "0.07325918868870857"],
|
||||
[1620758685, "0.07340909025275498"],
|
||||
[1620758700, "0.06640878600261439"],
|
||||
[1620758715, "0.016943481796378928"],
|
||||
[1620758730, "0.009846410786372045"],
|
||||
[1620758745, "0.009846533933076818"],
|
||||
[1620758760, "0.009865643995544734"],
|
||||
[1620758775, "0.009877495333796778"],
|
||||
[1620758790, "0.009894557340703772"],
|
||||
[1620758805, "0.0098843910341446"],
|
||||
[1620758820, "0.00990408341969324"],
|
||||
[1620758835, "0.00989844441243741"],
|
||||
[1620758850, "0.009889907575638773"],
|
||||
[1620758865, "0.009918898761738633"],
|
||||
[1620758880, "0.009937127911002756"],
|
||||
[1620758895, "0.009940908363410796"],
|
||||
[1620758910, "0.00998103477604732"],
|
||||
[1620758925, "0.009972785096318881"],
|
||||
[1620758940, "0.012851280416358784"],
|
||||
[1620758955, "0.016073228821362785"],
|
||||
[1620758970, "0.020414802032173343"],
|
||||
[1620761580, "0.007599075245347286"],
|
||||
[1620761595, "0.008931710803442608"],
|
||||
[1620761610, "0.008726716914241494"],
|
||||
[1620761625, "0.008200081743024097"],
|
||||
[1620761640, "0.00855242238708798"],
|
||||
[1620761655, "0.008286349295644651"],
|
||||
[1620761670, "0.008226278261449314"],
|
||||
[1620761685, "0.008195191146355274"],
|
||||
[1620761700, "0.008187372718523614"],
|
||||
[1620761715, "0.008513095070485845"],
|
||||
[1620761730, "0.08239661322810221"],
|
||||
[1620761745, "0.0859446307478243"],
|
||||
[1620761760, "0.08307358128715034"],
|
||||
[1620761775, "0.08068720480328369"],
|
||||
[1620761790, "0.07619009806120529"],
|
||||
[1620761805, "0.0750613052160521"],
|
||||
[1620761820, "0.07146092807229597"],
|
||||
[1620761835, "0.06898128960085806"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "success",
|
||||
"data": { "resultType": "vector", "result": [{ "metric": {}, "value": [1620761849, "0.06765848222986065"] }] }
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,136 +0,0 @@
|
||||
import CDP from 'chrome-remote-interface';
|
||||
import ProtocolProxyApi from 'devtools-protocol/types/protocol-proxy-api';
|
||||
import { countBy, mean } from 'lodash';
|
||||
import Tracelib, { TraceEvent } from 'tracelib';
|
||||
|
||||
import { CollectedData, DataCollector, DataCollectorName } from './DataCollector';
|
||||
|
||||
type CDPDataCollectorDeps = {
|
||||
port: number;
|
||||
};
|
||||
|
||||
export class CDPDataCollector implements DataCollector {
|
||||
private tracingCategories: string[];
|
||||
|
||||
private state: {
|
||||
client?: CDP.Client;
|
||||
tracingPromise?: Promise<CollectedData>;
|
||||
traceEvents: TraceEvent[];
|
||||
};
|
||||
|
||||
constructor(private deps: CDPDataCollectorDeps) {
|
||||
this.state = this.getDefaultState();
|
||||
this.tracingCategories = [
|
||||
'disabled-by-default-v8.cpu_profile',
|
||||
'disabled-by-default-v8.cpu_profiler',
|
||||
'disabled-by-default-v8.cpu_profiler.hires',
|
||||
'disabled-by-default-devtools.timeline.frame',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline.inputs',
|
||||
'disabled-by-default-devtools.timeline.stack',
|
||||
'disabled-by-default-devtools.timeline.invalidationTracking',
|
||||
'disabled-by-default-layout_shift.debug',
|
||||
'disabled-by-default-cc.debug.scheduler.frames',
|
||||
'disabled-by-default-blink.debug.display_lock',
|
||||
];
|
||||
}
|
||||
|
||||
getName = () => DataCollectorName.CDP;
|
||||
|
||||
private resetState = async () => {
|
||||
if (this.state.client) {
|
||||
await this.state.client.close();
|
||||
}
|
||||
this.state = this.getDefaultState();
|
||||
};
|
||||
|
||||
private getDefaultState = () => ({
|
||||
traceEvents: [],
|
||||
});
|
||||
|
||||
// workaround for type declaration issues in cdp lib
|
||||
private asApis = (
|
||||
client: CDP.Client
|
||||
): {
|
||||
Profiler: ProtocolProxyApi.ProfilerApi;
|
||||
Page: ProtocolProxyApi.PageApi;
|
||||
Tracing: ProtocolProxyApi.TracingApi;
|
||||
} => client;
|
||||
|
||||
private getClientApis = async () => this.asApis(await this.getClient());
|
||||
|
||||
private getClient = async () => {
|
||||
if (this.state.client) {
|
||||
return this.state.client;
|
||||
}
|
||||
|
||||
const client = await CDP({ port: this.deps.port });
|
||||
|
||||
const { Profiler, Page } = this.asApis(client);
|
||||
await Promise.all([Page.enable(), Profiler.enable(), Profiler.setSamplingInterval({ interval: 100 })]);
|
||||
|
||||
this.state.client = client;
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
start: DataCollector['start'] = async ({ id }) => {
|
||||
if (this.state.tracingPromise) {
|
||||
throw new Error(`collection in progress - can't start another one! ${id}`);
|
||||
}
|
||||
|
||||
const { Tracing, Profiler } = await this.getClientApis();
|
||||
|
||||
await Promise.all([
|
||||
Tracing.start({
|
||||
bufferUsageReportingInterval: 1000,
|
||||
traceConfig: {
|
||||
includedCategories: this.tracingCategories,
|
||||
},
|
||||
}),
|
||||
Profiler.start(),
|
||||
]);
|
||||
|
||||
Tracing.on('dataCollected', ({ value: events }) => {
|
||||
this.state.traceEvents.push(...events);
|
||||
});
|
||||
|
||||
let resolveFn: (data: CollectedData) => void;
|
||||
this.state.tracingPromise = new Promise<CollectedData>((resolve) => {
|
||||
resolveFn = resolve;
|
||||
});
|
||||
Tracing.on('tracingComplete', ({ dataLossOccurred }) => {
|
||||
const t = new Tracelib(this.state.traceEvents);
|
||||
|
||||
const eventCounts = countBy(this.state.traceEvents, (ev) => ev.name);
|
||||
|
||||
const fps = t.getFPS();
|
||||
|
||||
resolveFn({
|
||||
eventCounts,
|
||||
fps: mean(fps.values),
|
||||
tracingDataLoss: dataLossOccurred ? 1 : 0,
|
||||
warnings: t.getWarningCounts(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
stop: DataCollector['stop'] = async (req) => {
|
||||
if (!this.state.tracingPromise) {
|
||||
throw new Error(`collection was never started - there is nothing to stop!`);
|
||||
}
|
||||
|
||||
const { Tracing, Profiler } = await this.getClientApis();
|
||||
|
||||
// TODO: capture profiler data
|
||||
const [, , traceData] = await Promise.all([Profiler.stop(), Tracing.end(), this.state.tracingPromise]);
|
||||
|
||||
await this.resetState();
|
||||
|
||||
return traceData;
|
||||
};
|
||||
|
||||
close: DataCollector['close'] = async () => {
|
||||
await this.resetState();
|
||||
};
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
export type CollectedData = Record<string, unknown>;
|
||||
|
||||
export enum DataCollectorName {
|
||||
CDP = 'CDP',
|
||||
}
|
||||
|
||||
type DataCollectorRequest = { id: string };
|
||||
|
||||
export type DataCollector<T extends CollectedData = CollectedData> = {
|
||||
start: (input: DataCollectorRequest) => Promise<void>;
|
||||
stop: (input: DataCollectorRequest) => Promise<T>;
|
||||
getName: () => DataCollectorName;
|
||||
close: () => Promise<void>;
|
||||
};
|
@ -1,139 +0,0 @@
|
||||
import { fromPairs } from 'lodash';
|
||||
|
||||
import { CollectedData, DataCollectorName } from './DataCollector';
|
||||
|
||||
type Stats = {
|
||||
sum: number;
|
||||
min: number;
|
||||
max: number;
|
||||
count: number;
|
||||
avg: number;
|
||||
time: number;
|
||||
};
|
||||
|
||||
export enum MeasurementName {
|
||||
DataRenderDelay = 'DataRenderDelay',
|
||||
}
|
||||
|
||||
type LivePerformanceAppStats = Record<MeasurementName, Stats[]>;
|
||||
|
||||
const isLivePerformanceAppStats = (data: CollectedData[]): data is LivePerformanceAppStats[] =>
|
||||
data.some((st) => {
|
||||
const stat = st?.[MeasurementName.DataRenderDelay];
|
||||
return Array.isArray(stat) && Boolean(stat?.length);
|
||||
});
|
||||
|
||||
type FormattedStats = {
|
||||
total: {
|
||||
count: number[];
|
||||
avg: number[];
|
||||
};
|
||||
lastInterval: {
|
||||
avg: number[];
|
||||
min: number[];
|
||||
max: number[];
|
||||
count: number[];
|
||||
};
|
||||
};
|
||||
|
||||
export const formatAppStats = (allStats: CollectedData[]) => {
|
||||
if (!isLivePerformanceAppStats(allStats)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const names = Object.keys(MeasurementName) as MeasurementName[];
|
||||
|
||||
return fromPairs(
|
||||
names.map((name) => {
|
||||
const statsForMeasurement = allStats.map((s) => s[name]);
|
||||
const res: FormattedStats = {
|
||||
total: {
|
||||
count: [],
|
||||
avg: [],
|
||||
},
|
||||
lastInterval: {
|
||||
avg: [],
|
||||
min: [],
|
||||
max: [],
|
||||
count: [],
|
||||
},
|
||||
};
|
||||
|
||||
statsForMeasurement.forEach((s) => {
|
||||
const total = s.reduce(
|
||||
(prev, next) => {
|
||||
prev.count += next.count;
|
||||
prev.avg += next.avg;
|
||||
return prev;
|
||||
},
|
||||
{ count: 0, avg: 0 }
|
||||
);
|
||||
res.total.count.push(Math.round(total.count));
|
||||
res.total.avg.push(Math.round(total.avg / s.length));
|
||||
|
||||
const lastInterval = s[s.length - 1];
|
||||
|
||||
res.lastInterval.avg.push(Math.round(lastInterval?.avg));
|
||||
res.lastInterval.min.push(Math.round(lastInterval?.min));
|
||||
res.lastInterval.max.push(Math.round(lastInterval?.max));
|
||||
res.lastInterval.count.push(Math.round(lastInterval?.count));
|
||||
});
|
||||
|
||||
return [name, res];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
type CDPData = {
|
||||
eventCounts: Record<string, unknown>;
|
||||
fps: number;
|
||||
tracingDataLoss: number;
|
||||
warnings: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const isCDPData = (data: any[]): data is CDPData[] => data.every((d) => typeof d.eventCounts === 'object');
|
||||
|
||||
type FormattedCDPData = {
|
||||
minorGC: number[];
|
||||
majorGC: number[];
|
||||
droppedFrames: number[];
|
||||
fps: number[];
|
||||
tracingDataLossOccurred: boolean;
|
||||
longTaskWarnings: number[];
|
||||
};
|
||||
|
||||
const emptyFormattedCDPData = (): FormattedCDPData => ({
|
||||
minorGC: [],
|
||||
majorGC: [],
|
||||
droppedFrames: [],
|
||||
fps: [],
|
||||
tracingDataLossOccurred: false,
|
||||
longTaskWarnings: [],
|
||||
});
|
||||
|
||||
const formatCDPData = (data: any): FormattedCDPData => {
|
||||
if (!isCDPData(data)) {
|
||||
return emptyFormattedCDPData();
|
||||
}
|
||||
|
||||
return data.reduce((acc, next) => {
|
||||
acc.majorGC.push((next.eventCounts.MajorGC as number) ?? 0);
|
||||
acc.minorGC.push((next.eventCounts.MinorGC as number) ?? 0);
|
||||
acc.fps.push(Math.round(next.fps) ?? 0);
|
||||
acc.tracingDataLossOccurred = acc.tracingDataLossOccurred || Boolean(next.tracingDataLoss);
|
||||
acc.droppedFrames.push((next.eventCounts.DroppedFrame as number) ?? 0);
|
||||
acc.longTaskWarnings.push((next.warnings.LongTask as number) ?? 0);
|
||||
return acc;
|
||||
}, emptyFormattedCDPData());
|
||||
};
|
||||
|
||||
export const formatResults = (
|
||||
results: Array<{ appStats: CollectedData; collectorsData: CollectedData }>
|
||||
): CollectedData => {
|
||||
return {
|
||||
...formatAppStats(results.map(({ appStats }) => appStats)),
|
||||
...formatCDPData(results.map(({ collectorsData }) => collectorsData[DataCollectorName.CDP])),
|
||||
|
||||
__raw: results,
|
||||
};
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import { fromPairs } from 'lodash';
|
||||
|
||||
import { CDPDataCollector } from './CDPDataCollector';
|
||||
import { CollectedData, DataCollector } from './DataCollector';
|
||||
import { formatResults } from './formatting';
|
||||
const remoteDebuggingPortOptionPrefix = '--remote-debugging-port=';
|
||||
|
||||
const getOrAddRemoteDebuggingPort = (args: string[]) => {
|
||||
const existing = args.find((arg) => arg.startsWith(remoteDebuggingPortOptionPrefix));
|
||||
|
||||
if (existing) {
|
||||
return Number(existing.substring(remoteDebuggingPortOptionPrefix.length));
|
||||
}
|
||||
|
||||
const port = 40000 + Math.round(Math.random() * 25000);
|
||||
args.push(`${remoteDebuggingPortOptionPrefix}${port}`);
|
||||
return port;
|
||||
};
|
||||
|
||||
let collectors: DataCollector[] = [];
|
||||
let results: Array<{ appStats: CollectedData; collectorsData: CollectedData }> = [];
|
||||
|
||||
const startBenchmarking = async ({ testName }: { testName: string }) => {
|
||||
await Promise.all(collectors.map((coll) => coll.start({ id: testName })));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const stopBenchmarking = async ({ testName, appStats }: { testName: string; appStats: CollectedData }) => {
|
||||
const data = await Promise.all(collectors.map(async (coll) => [coll.getName(), await coll.stop({ id: testName })]));
|
||||
|
||||
results.push({
|
||||
collectorsData: fromPairs(data),
|
||||
appStats: appStats,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
const afterRun = async () => {
|
||||
await Promise.all(collectors.map((coll) => coll.close()));
|
||||
collectors = [];
|
||||
results = [];
|
||||
};
|
||||
|
||||
const afterSpec = (resultsFolder: string) => async (spec: { name: string }) => {
|
||||
fs.writeFileSync(`${resultsFolder}/${spec.name}-${Date.now()}.json`, JSON.stringify(formatResults(results), null, 2));
|
||||
|
||||
results = [];
|
||||
};
|
||||
|
||||
export const initialize: Cypress.PluginConfig = (on, config) => {
|
||||
const resultsFolder = config.env['BENCHMARK_PLUGIN_RESULTS_FOLDER'];
|
||||
|
||||
if (!fs.existsSync(resultsFolder)) {
|
||||
fs.mkdirSync(resultsFolder, { recursive: true });
|
||||
console.log(`Created folder for benchmark results ${resultsFolder}`);
|
||||
}
|
||||
|
||||
on('before:browser:launch', async (browser, options) => {
|
||||
if (browser.family !== 'chromium' || browser.name === 'electron') {
|
||||
throw new Error('benchmarking plugin requires chrome');
|
||||
}
|
||||
|
||||
const { args } = options;
|
||||
|
||||
const port = getOrAddRemoteDebuggingPort(args);
|
||||
collectors.push(new CDPDataCollector({ port }));
|
||||
|
||||
args.push('--start-fullscreen');
|
||||
|
||||
console.log(
|
||||
`initialized benchmarking plugin with ${collectors.length} collectors: ${collectors
|
||||
.map((col) => col.getName())
|
||||
.join(', ')}`
|
||||
);
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
on('task', {
|
||||
startBenchmarking,
|
||||
stopBenchmarking,
|
||||
});
|
||||
|
||||
on('after:run', afterRun);
|
||||
on('after:spec', afterSpec(resultsFolder));
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
type TraceEvent = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
declare class Tracelib {
|
||||
constructor(private events: TraceEvent[]) {}
|
||||
|
||||
getFPS: () => { times: number[]; values: number[] };
|
||||
getWarningCounts: () => Record<string, number>;
|
||||
}
|
||||
declare module 'tracelib' {
|
||||
export = Tracelib;
|
||||
|
||||
export { TraceEvent };
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
'use strict';
|
||||
const BlinkDiff = require('blink-diff');
|
||||
const { resolve } = require('path');
|
||||
|
||||
// @todo use npmjs.com/pixelmatch or an available cypress plugin
|
||||
const compareScreenshots = async ({ config, screenshotsFolder, specName }) => {
|
||||
const name = config.name || config; // @todo use `??`
|
||||
const threshold = config.threshold || 0.001; // @todo use `??`
|
||||
|
||||
const imageAPath = `${screenshotsFolder}/${specName}/${name}.png`;
|
||||
const imageBPath = resolve(`${screenshotsFolder}/../expected/${specName}/${name}.png`);
|
||||
|
||||
const imageOutputPath = screenshotsFolder.endsWith('actual') ? imageAPath.replace('.png', '.diff.png') : undefined;
|
||||
|
||||
const { code } = await new Promise((resolve, reject) => {
|
||||
new BlinkDiff({
|
||||
imageAPath,
|
||||
imageBPath,
|
||||
imageOutputPath,
|
||||
threshold,
|
||||
thresholdType: BlinkDiff.THRESHOLD_PERCENT,
|
||||
}).run((error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (code <= 1) {
|
||||
let msg = `\nThe screenshot [${imageAPath}] differs from [${imageBPath}]`;
|
||||
msg += '\n';
|
||||
msg += '\nCheck the Artifacts tab in the CircleCi build output for the actual screenshots.';
|
||||
msg += '\n';
|
||||
msg += '\n If the difference between expected and outcome is NOT acceptable then do the following:';
|
||||
msg += '\n - Check the code for changes that causes this difference, fix that and retry.';
|
||||
msg += '\n';
|
||||
msg += '\n If the difference between expected and outcome is acceptable then do the following:';
|
||||
msg += '\n - Replace the expected image with the outcome and retry.';
|
||||
msg += '\n';
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
// Must return a value
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = compareScreenshots;
|
@ -1,79 +0,0 @@
|
||||
'use strict';
|
||||
const {
|
||||
promises: { readFile },
|
||||
} = require('fs');
|
||||
const { resolve } = require('path');
|
||||
|
||||
// @todo use https://github.com/bahmutov/cypress-extends when possible
|
||||
module.exports = async (baseConfig) => {
|
||||
// From CLI
|
||||
const {
|
||||
env: { CWD, UPDATE_SCREENSHOTS },
|
||||
} = baseConfig;
|
||||
|
||||
if (CWD) {
|
||||
// @todo: https://github.com/cypress-io/cypress/issues/6406
|
||||
const jsonReporter = require.resolve('@mochajs/json-file-reporter');
|
||||
|
||||
// @todo `baseUrl: env.CYPRESS_BASEURL`
|
||||
const projectConfig = {
|
||||
fixturesFolder: `${CWD}/cypress/fixtures`,
|
||||
integrationFolder: `${CWD}/cypress/integration`,
|
||||
reporter: jsonReporter,
|
||||
reporterOptions: {
|
||||
output: `${CWD}/cypress/report.json`,
|
||||
},
|
||||
screenshotsFolder: `${CWD}/cypress/screenshots/${UPDATE_SCREENSHOTS ? 'expected' : 'actual'}`,
|
||||
videosFolder: `${CWD}/cypress/videos`,
|
||||
};
|
||||
|
||||
const customProjectConfig = await readFile(`${CWD}/cypress.json`, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.then((config) => {
|
||||
const pathKeys = [
|
||||
'fileServerFolder',
|
||||
'fixturesFolder',
|
||||
'ignoreTestFiles',
|
||||
'integrationFolder',
|
||||
'pluginsFile',
|
||||
'screenshotsFolder',
|
||||
'supportFile',
|
||||
'testFiles',
|
||||
'videosFolder',
|
||||
];
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(config).map(([key, value]) => {
|
||||
if (pathKeys.includes(key)) {
|
||||
return [key, resolve(CWD, value)];
|
||||
} else {
|
||||
return [key, value];
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
// File is optional
|
||||
return {};
|
||||
} else {
|
||||
// Unexpected error
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
...projectConfig,
|
||||
...customProjectConfig,
|
||||
reporterOptions: {
|
||||
...baseConfig.reporterOptions,
|
||||
...projectConfig.reporterOptions,
|
||||
...customProjectConfig.reporterOptions,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Temporary legacy support for Grafana core (using `yarn start`)
|
||||
return baseConfig;
|
||||
}
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const benchmarkPlugin = require('./benchmark');
|
||||
const compareScreenshots = require('./compareScreenshots');
|
||||
const extendConfig = require('./extendConfig');
|
||||
const readProvisions = require('./readProvisions');
|
||||
const typescriptPreprocessor = require('./typescriptPreprocessor');
|
||||
|
||||
module.exports = (on, config) => {
|
||||
if (config.env['BENCHMARK_PLUGIN_ENABLED'] === true) {
|
||||
benchmarkPlugin.initialize(on, config);
|
||||
}
|
||||
|
||||
on('file:preprocessor', typescriptPreprocessor);
|
||||
on('task', { compareScreenshots, readProvisions });
|
||||
on('task', {
|
||||
log({ message, optional }) {
|
||||
optional ? console.log(message, optional) : console.log(message);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
on('task', {
|
||||
getJSONFilesFromDir: async ({ projectPath, relativePath }) => {
|
||||
const directoryPath = path.join(projectPath, relativePath);
|
||||
const jsonFiles = fs.readdirSync(directoryPath);
|
||||
return jsonFiles
|
||||
.filter((fileName) => /.json$/i.test(fileName))
|
||||
.map((fileName) => {
|
||||
const fileBuffer = fs.readFileSync(path.join(directoryPath, fileName));
|
||||
return JSON.parse(fileBuffer);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Make recordings higher resolution
|
||||
// https://www.cypress.io/blog/2021/03/01/generate-high-resolution-videos-and-screenshots/
|
||||
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||
console.log('launching browser %s is headless? %s', browser.name, browser.isHeadless);
|
||||
|
||||
// the browser width and height we want to get
|
||||
// our screenshots and videos will be of that resolution
|
||||
const width = 1920;
|
||||
const height = 1080;
|
||||
|
||||
console.log('setting the browser window size to %d x %d', width, height);
|
||||
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
launchOptions.args.push(`--window-size=${width},${height}`);
|
||||
|
||||
// force screen to be non-retina and just use our given resolution
|
||||
launchOptions.args.push('--force-device-scale-factor=1');
|
||||
}
|
||||
|
||||
if (browser.name === 'electron' && browser.isHeadless) {
|
||||
// might not work on CI for some reason
|
||||
launchOptions.preferences.width = width;
|
||||
launchOptions.preferences.height = height;
|
||||
}
|
||||
|
||||
if (browser.name === 'firefox' && browser.isHeadless) {
|
||||
launchOptions.args.push(`--width=${width}`);
|
||||
launchOptions.args.push(`--height=${height}`);
|
||||
}
|
||||
|
||||
// IMPORTANT: return the updated browser launch options
|
||||
return launchOptions;
|
||||
});
|
||||
|
||||
// Always extend with this library's config and return for diffing
|
||||
// @todo remove this when possible: https://github.com/cypress-io/cypress/issues/5674
|
||||
return extendConfig(config);
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
'use strict';
|
||||
const {
|
||||
promises: { readFile },
|
||||
} = require('fs');
|
||||
const { resolve: resolvePath } = require('path');
|
||||
const { parse: parseYml } = require('yaml');
|
||||
|
||||
const readProvision = (filePath) => readFile(filePath, 'utf8').then((contents) => parseYml(contents));
|
||||
|
||||
const readProvisions = (filePaths) => Promise.all(filePaths.map(readProvision));
|
||||
|
||||
// Paths are relative to <project-root>/provisioning
|
||||
module.exports = ({ CWD, filePaths }) =>
|
||||
readProvisions(filePaths.map((filePath) => resolvePath(CWD, 'provisioning', filePath)));
|
@ -1,42 +0,0 @@
|
||||
const wp = require('@cypress/webpack-preprocessor');
|
||||
const { resolve } = require('path');
|
||||
|
||||
const anyNodeModules = /node_modules/;
|
||||
const packageRoot = resolve(`${__dirname}/../../`);
|
||||
const packageModules = `${packageRoot}/node_modules`;
|
||||
|
||||
const webpackOptions = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
include: (modulePath) => {
|
||||
if (!anyNodeModules.test(modulePath)) {
|
||||
// Is a file within the project
|
||||
return true;
|
||||
} else {
|
||||
// Is a file within this package
|
||||
return modulePath.startsWith(packageRoot) && !modulePath.startsWith(packageModules);
|
||||
}
|
||||
},
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
webpackOptions,
|
||||
};
|
||||
|
||||
module.exports = wp(options);
|
@ -1,41 +0,0 @@
|
||||
import 'cypress-file-upload';
|
||||
|
||||
interface CompareScreenshotsConfig {
|
||||
name: string;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
Cypress.Commands.add('compareScreenshots', (config: CompareScreenshotsConfig | string) => {
|
||||
cy.task('compareScreenshots', {
|
||||
config,
|
||||
screenshotsFolder: Cypress.config('screenshotsFolder'),
|
||||
specName: Cypress.spec.name,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('logToConsole', (message: string, optional?: any) => {
|
||||
cy.task('log', { message: '(' + new Date().toISOString() + ') ' + message, optional });
|
||||
});
|
||||
|
||||
Cypress.Commands.add('readProvisions', (filePaths: string[]) => {
|
||||
cy.task('readProvisions', {
|
||||
CWD: Cypress.env('CWD'),
|
||||
filePaths,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getJSONFilesFromDir', (dirPath: string) => {
|
||||
return cy.task('getJSONFilesFromDir', {
|
||||
// CWD is set for plugins in the cli but not for the main grafana repo: https://github.com/grafana/grafana/blob/main/packages/grafana-e2e/cli.js#L12
|
||||
projectPath: Cypress.env('CWD') || Cypress.config().parentTestsFolder,
|
||||
relativePath: dirPath,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('startBenchmarking', (testName: string) => {
|
||||
return cy.task('startBenchmarking', { testName });
|
||||
});
|
||||
|
||||
Cypress.Commands.add('stopBenchmarking', (testName: string, appStats: Record<string, unknown>) => {
|
||||
return cy.task('stopBenchmarking', { testName, appStats });
|
||||
});
|
12
packages/grafana-e2e/cypress/support/index.d.ts
vendored
12
packages/grafana-e2e/cypress/support/index.d.ts
vendored
@ -1,12 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
compareScreenshots(config: CompareScreenshotsConfig | string): Chainable;
|
||||
logToConsole(message: string, optional?: any): void;
|
||||
readProvisions(filePaths: string[]): Chainable;
|
||||
getJSONFilesFromDir(dirPath: string): Chainable;
|
||||
startBenchmarking(testName: string): void;
|
||||
stopBenchmarking(testName: string, appStats: Record<string, unknown>): void;
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
// yarn build fails with:
|
||||
// >> /Users/hugo/go/src/github.com/grafana/grafana/node_modules/stringmap/stringmap.js:99
|
||||
// >> throw new Error("StringMap expected string key");
|
||||
// require('cypress-failed-log');
|
||||
import './commands';
|
||||
|
||||
Cypress.Screenshot.defaults({
|
||||
screenshotOnRunFailure: false,
|
||||
});
|
||||
|
||||
const COMMAND_DELAY = 1000;
|
||||
|
||||
if (Cypress.env('SLOWMO')) {
|
||||
const commandsToModify = ['clear', 'click', 'contains', 'reload', 'then', 'trigger', 'type', 'visit'];
|
||||
|
||||
commandsToModify.forEach((command) => {
|
||||
// @ts-ignore -- https://github.com/cypress-io/cypress/issues/7807
|
||||
Cypress.Commands.overwrite(command, (originalFn, ...args) => {
|
||||
const origVal = originalFn(...args);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(origVal), COMMAND_DELAY);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// @todo remove when possible: https://github.com/cypress-io/cypress/issues/95
|
||||
Cypress.on('window:before:load', (win) => {
|
||||
// @ts-ignore
|
||||
delete win.fetch;
|
||||
});
|
||||
|
||||
// See https://github.com/quasarframework/quasar/issues/2233 for details
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
||||
Cypress.on('uncaught:exception', (err) => {
|
||||
/* returning false here prevents Cypress from failing the test */
|
||||
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// uncomment below to prevent Cypress from failing tests when unhandled errors are thrown
|
||||
// Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// // returning false here prevents Cypress from
|
||||
// // failing the test
|
||||
// return false;
|
||||
// });
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"module": "commonjs",
|
||||
"types": ["cypress", "cypress-file-upload", "node"]
|
||||
},
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["**/*.ts"]
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
{
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "11.1.0-pre",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"grafana",
|
||||
"e2e",
|
||||
"typescript"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git",
|
||||
"directory": "packages/grafana-e2e"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"bin": {
|
||||
"grafana-e2e": "bin/grafana-e2e.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"cypress",
|
||||
"dist",
|
||||
"cli.js",
|
||||
"cypress.json",
|
||||
"./README.md",
|
||||
"./CHANGELOG.md",
|
||||
"LICENSE_APACHE2"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
|
||||
"bundle": "rollup -c rollup.config.ts",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"open": "cypress open",
|
||||
"start": "cypress run --browser=chrome",
|
||||
"start-benchmark": "CYPRESS_NO_COMMAND_LOG=1 yarn start",
|
||||
"test": "pushd test && node ../dist/bin/grafana-e2e.js run",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@types/chrome-remote-interface": "0.31.10",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/node": "18.18.4",
|
||||
"@types/uuid": "9.0.2",
|
||||
"esbuild": "0.18.12",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-esbuild": "5.0.0",
|
||||
"rollup-plugin-node-externals": "^5.0.0",
|
||||
"webpack": "5.89.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.23.2",
|
||||
"@babel/preset-env": "7.23.2",
|
||||
"@cypress/webpack-preprocessor": "5.17.1",
|
||||
"@grafana/e2e-selectors": "11.1.0-pre",
|
||||
"@grafana/schema": "11.1.0-pre",
|
||||
"@grafana/tsconfig": "^1.3.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"blink-diff": "1.0.13",
|
||||
"chrome-remote-interface": "0.33.0",
|
||||
"commander": "8.3.0",
|
||||
"cypress": "9.5.1",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"devtools-protocol": "0.0.1170333",
|
||||
"execa": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"mocha": "10.2.0",
|
||||
"resolve-bin": "1.0.1",
|
||||
"rimraf": "5.0.1",
|
||||
"tracelib": "1.0.1",
|
||||
"ts-loader": "8.4.0",
|
||||
"tslib": "2.6.0",
|
||||
"typescript": "5.2.2",
|
||||
"uuid": "9.0.0",
|
||||
"yaml": "^2.0.0"
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { externals } from 'rollup-plugin-node-externals';
|
||||
|
||||
const pkg = require('./package.json');
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild({ target: 'node16' })],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
},
|
||||
];
|
@ -1,303 +0,0 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { e2e } from '../index';
|
||||
import { getDashboardUid } from '../support/url';
|
||||
|
||||
import { DeleteDashboardConfig } from './deleteDashboard';
|
||||
import { selectOption } from './selectOption';
|
||||
import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange';
|
||||
|
||||
export interface AddAnnotationConfig {
|
||||
dataSource: string;
|
||||
dataSourceForm?: () => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AddDashboardConfig {
|
||||
annotations: AddAnnotationConfig[];
|
||||
timeRange: TimeRangeConfig;
|
||||
title: string;
|
||||
variables: PartialAddVariableConfig[];
|
||||
}
|
||||
|
||||
interface AddVariableDefault {
|
||||
hide: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface AddVariableOptional {
|
||||
constantValue?: string;
|
||||
dataSource?: string;
|
||||
label?: string;
|
||||
query?: string;
|
||||
regex?: string;
|
||||
variableQueryForm?: (config: AddVariableConfig) => void;
|
||||
}
|
||||
|
||||
interface AddVariableRequired {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type PartialAddVariableConfig = Partial<AddVariableDefault> & AddVariableOptional & AddVariableRequired;
|
||||
export type AddVariableConfig = AddVariableDefault & AddVariableOptional & AddVariableRequired;
|
||||
|
||||
/**
|
||||
* This flow is used to add a dashboard with whatever configuration specified.
|
||||
* @param config Configuration object. Currently supports configuring dashboard time range, annotations, and variables (support dependant on type).
|
||||
* @see{@link AddDashboardConfig}
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* // Configuring a simple dashboard
|
||||
* addDashboard({
|
||||
* timeRange: {
|
||||
* from: '2022-10-03 00:00:00',
|
||||
* to: '2022-10-03 23:59:59',
|
||||
* zone: 'Coordinated Universal Time',
|
||||
* },
|
||||
* title: 'Test Dashboard',
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* // Configuring a dashboard with annotations
|
||||
* addDashboard({
|
||||
* title: 'Test Dashboard',
|
||||
* annotations: [
|
||||
* {
|
||||
* // This should match the datasource name
|
||||
* dataSource: 'azure-monitor',
|
||||
* name: 'Test Annotation',
|
||||
* dataSourceForm: () => {
|
||||
* // Insert steps to create annotation using datasource form
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @see{@link AddAnnotationConfig}
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* // Configuring a dashboard with variables
|
||||
* addDashboard({
|
||||
* title: 'Test Dashboard',
|
||||
* variables: [
|
||||
* {
|
||||
* name: 'test-query-variable',
|
||||
* label: 'Testing Query',
|
||||
* hide: '',
|
||||
* type: e2e.flows.VARIABLE_TYPE_QUERY,
|
||||
* dataSource: 'azure-monitor',
|
||||
* variableQueryForm: () => {
|
||||
* // Insert steps to create variable using datasource form
|
||||
* },
|
||||
* },
|
||||
* {
|
||||
* name: 'test-constant-variable',
|
||||
* label: 'Testing Constant',
|
||||
* type: e2e.flows.VARIABLE_TYPE_CONSTANT,
|
||||
* constantValue: 'constant',
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @see{@link AddVariableConfig}
|
||||
*
|
||||
* @see{@link https://github.com/grafana/grafana/blob/main/e2e/cloud-plugins-suite/azure-monitor.spec.ts Azure Monitor Tests for full examples}
|
||||
*/
|
||||
export const addDashboard = (config?: Partial<AddDashboardConfig>) => {
|
||||
const fullConfig: AddDashboardConfig = {
|
||||
annotations: [],
|
||||
title: `e2e-${uuidv4()}`,
|
||||
variables: [],
|
||||
...config,
|
||||
timeRange: {
|
||||
from: '2020-01-01 00:00:00',
|
||||
to: '2020-01-01 06:00:00',
|
||||
zone: 'Coordinated Universal Time',
|
||||
...config?.timeRange,
|
||||
},
|
||||
};
|
||||
|
||||
const { annotations, timeRange, title, variables } = fullConfig;
|
||||
|
||||
e2e().logToConsole('Adding dashboard with title:', title);
|
||||
|
||||
e2e.pages.AddDashboard.visit();
|
||||
|
||||
if (annotations.length > 0 || variables.length > 0) {
|
||||
e2e.components.PageToolbar.item('Dashboard settings').click();
|
||||
addAnnotations(annotations);
|
||||
|
||||
fullConfig.variables = addVariables(variables);
|
||||
|
||||
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
|
||||
}
|
||||
|
||||
setDashboardTimeRange(timeRange);
|
||||
|
||||
e2e.components.PageToolbar.item('Save dashboard').click();
|
||||
e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true });
|
||||
e2e.pages.SaveDashboardAsModal.save().click();
|
||||
e2e.flows.assertSuccessNotification();
|
||||
e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible');
|
||||
|
||||
e2e().logToConsole('Added dashboard with title:', title);
|
||||
|
||||
return e2e()
|
||||
.url()
|
||||
.should('contain', '/d/')
|
||||
.then((url: string) => {
|
||||
const uid = getDashboardUid(url);
|
||||
|
||||
e2e.getScenarioContext().then(({ addedDashboards }: any) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDashboards: [...addedDashboards, { title, uid } as DeleteDashboardConfig],
|
||||
});
|
||||
});
|
||||
|
||||
// @todo remove `wrap` when possible
|
||||
return e2e().wrap(
|
||||
{
|
||||
config: fullConfig,
|
||||
uid,
|
||||
},
|
||||
{ log: false }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const addAnnotation = (config: AddAnnotationConfig, isFirst: boolean) => {
|
||||
if (isFirst) {
|
||||
if (e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2) {
|
||||
e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2().click();
|
||||
} else {
|
||||
e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTA().click();
|
||||
}
|
||||
} else {
|
||||
cy.contains('New query').click();
|
||||
}
|
||||
|
||||
const { dataSource, dataSourceForm, name } = config;
|
||||
|
||||
selectOption({
|
||||
container: e2e.components.DataSourcePicker.container(),
|
||||
optionText: dataSource,
|
||||
});
|
||||
|
||||
e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type(name);
|
||||
|
||||
if (dataSourceForm) {
|
||||
dataSourceForm();
|
||||
}
|
||||
};
|
||||
|
||||
const addAnnotations = (configs: AddAnnotationConfig[]) => {
|
||||
if (configs.length > 0) {
|
||||
e2e.pages.Dashboard.Settings.General.sectionItems('Annotations').click();
|
||||
}
|
||||
|
||||
return configs.forEach((config, i) => addAnnotation(config, i === 0));
|
||||
};
|
||||
|
||||
export const VARIABLE_HIDE_LABEL = 'Label';
|
||||
export const VARIABLE_HIDE_NOTHING = '';
|
||||
export const VARIABLE_HIDE_VARIABLE = 'Variable';
|
||||
|
||||
export const VARIABLE_TYPE_AD_HOC_FILTERS = 'Ad hoc filters';
|
||||
export const VARIABLE_TYPE_CONSTANT = 'Constant';
|
||||
export const VARIABLE_TYPE_DATASOURCE = 'Datasource';
|
||||
export const VARIABLE_TYPE_QUERY = 'Query';
|
||||
|
||||
const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVariableConfig => {
|
||||
const fullConfig = {
|
||||
hide: VARIABLE_HIDE_NOTHING,
|
||||
type: VARIABLE_TYPE_QUERY,
|
||||
...config,
|
||||
};
|
||||
|
||||
if (isFirst) {
|
||||
if (e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2) {
|
||||
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2().click();
|
||||
} else {
|
||||
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
|
||||
}
|
||||
} else {
|
||||
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
|
||||
}
|
||||
|
||||
const { constantValue, dataSource, label, name, query, regex, type, variableQueryForm } = fullConfig;
|
||||
|
||||
// This field is key to many reactive changes
|
||||
if (type !== VARIABLE_TYPE_QUERY) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.singleValue().should('have.text', 'Query').parent().click();
|
||||
});
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().find('input').type(`${type}{enter}`);
|
||||
}
|
||||
|
||||
if (label) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label);
|
||||
}
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name);
|
||||
|
||||
if (
|
||||
dataSource &&
|
||||
(type === VARIABLE_TYPE_AD_HOC_FILTERS || type === VARIABLE_TYPE_DATASOURCE || type === VARIABLE_TYPE_QUERY)
|
||||
) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.DataSourcePicker.inputV2().type(`${dataSource}{enter}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (constantValue && type === VARIABLE_TYPE_CONSTANT) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type(constantValue);
|
||||
}
|
||||
|
||||
if (type === VARIABLE_TYPE_QUERY) {
|
||||
if (query) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().type(query);
|
||||
}
|
||||
|
||||
if (regex) {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2().type(regex);
|
||||
}
|
||||
|
||||
if (variableQueryForm) {
|
||||
variableQueryForm(fullConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid flakiness
|
||||
e2e().focused().blur();
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
|
||||
.should('exist')
|
||||
.within((previewOfValues) => {
|
||||
if (type === VARIABLE_TYPE_CONSTANT) {
|
||||
expect(previewOfValues.text()).equals(constantValue);
|
||||
}
|
||||
});
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
|
||||
|
||||
return fullConfig;
|
||||
};
|
||||
|
||||
const addVariables = (configs: PartialAddVariableConfig[]): AddVariableConfig[] => {
|
||||
if (configs.length > 0) {
|
||||
e2e.components.Tab.title('Variables').click();
|
||||
}
|
||||
|
||||
return configs.map((config, i) => addVariable(config, i === 0));
|
||||
};
|
@ -1,116 +0,0 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { e2e } from '../index';
|
||||
|
||||
import { DeleteDataSourceConfig } from './deleteDataSource';
|
||||
|
||||
export interface AddDataSourceConfig {
|
||||
basicAuth: boolean;
|
||||
basicAuthPassword: string;
|
||||
basicAuthUser: string;
|
||||
expectedAlertMessage: string | RegExp;
|
||||
form: () => void;
|
||||
name: string;
|
||||
skipTlsVerify: boolean;
|
||||
type: string;
|
||||
timeout?: number;
|
||||
awaitHealth?: boolean;
|
||||
}
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable<AddDaaSourceConfig>`
|
||||
export const addDataSource = (config?: Partial<AddDataSourceConfig>) => {
|
||||
const fullConfig: AddDataSourceConfig = {
|
||||
basicAuth: false,
|
||||
basicAuthPassword: '',
|
||||
basicAuthUser: '',
|
||||
expectedAlertMessage: 'Data source is working',
|
||||
form: () => {},
|
||||
name: `e2e-${uuidv4()}`,
|
||||
skipTlsVerify: false,
|
||||
type: 'TestData',
|
||||
...config,
|
||||
};
|
||||
|
||||
const {
|
||||
basicAuth,
|
||||
basicAuthPassword,
|
||||
basicAuthUser,
|
||||
expectedAlertMessage,
|
||||
form,
|
||||
name,
|
||||
skipTlsVerify,
|
||||
type,
|
||||
timeout,
|
||||
awaitHealth,
|
||||
} = fullConfig;
|
||||
|
||||
if (awaitHealth) {
|
||||
e2e()
|
||||
.intercept(/health/)
|
||||
.as('health');
|
||||
}
|
||||
|
||||
e2e().logToConsole('Adding data source with name:', name);
|
||||
e2e.pages.AddDataSource.visit();
|
||||
e2e.pages.AddDataSource.dataSourcePluginsV2(type)
|
||||
.scrollIntoView()
|
||||
.should('be.visible') // prevents flakiness
|
||||
.click();
|
||||
|
||||
e2e.pages.DataSource.name().clear();
|
||||
e2e.pages.DataSource.name().type(name);
|
||||
|
||||
if (basicAuth) {
|
||||
e2e().contains('label', 'Basic auth').scrollIntoView().click();
|
||||
e2e()
|
||||
.contains('.gf-form-group', 'Basic Auth Details')
|
||||
.should('be.visible')
|
||||
.scrollIntoView()
|
||||
.within(() => {
|
||||
if (basicAuthUser) {
|
||||
e2e().get('[placeholder=user]').type(basicAuthUser);
|
||||
}
|
||||
if (basicAuthPassword) {
|
||||
e2e().get('[placeholder=Password]').type(basicAuthPassword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (skipTlsVerify) {
|
||||
e2e().contains('label', 'Skip TLS Verify').scrollIntoView().click();
|
||||
}
|
||||
|
||||
form();
|
||||
|
||||
e2e.pages.DataSource.saveAndTest().click();
|
||||
|
||||
if (awaitHealth) {
|
||||
e2e().wait('@health', { timeout: timeout ?? e2e.config().defaultCommandTimeout });
|
||||
}
|
||||
|
||||
// use the timeout passed in if it exists, otherwise, continue to use the default
|
||||
e2e.pages.DataSource.alert()
|
||||
.should('exist')
|
||||
.contains(expectedAlertMessage, {
|
||||
timeout: timeout ?? e2e.config().defaultCommandTimeout,
|
||||
});
|
||||
e2e().logToConsole('Added data source with name:', name);
|
||||
|
||||
return e2e()
|
||||
.url()
|
||||
.then(() => {
|
||||
e2e.getScenarioContext().then(({ addedDataSources }: any) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDataSources: [...addedDataSources, { name } as DeleteDataSourceConfig],
|
||||
});
|
||||
});
|
||||
|
||||
// @todo remove `wrap` when possible
|
||||
return e2e().wrap(
|
||||
{
|
||||
config: fullConfig,
|
||||
},
|
||||
{ log: false }
|
||||
);
|
||||
});
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getScenarioContext } from '../support/scenarioContext';
|
||||
|
||||
import { configurePanel, PartialAddPanelConfig } from './configurePanel';
|
||||
|
||||
export const addPanel = (config?: Partial<PartialAddPanelConfig>) =>
|
||||
getScenarioContext().then(({ lastAddedDataSource }: any) =>
|
||||
configurePanel({
|
||||
dataSourceName: lastAddedDataSource,
|
||||
panelTitle: `e2e-${uuidv4()}`,
|
||||
...config,
|
||||
isEdit: false,
|
||||
})
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const assertSuccessNotification = () => {
|
||||
if (e2e.components.Alert.alertV2) {
|
||||
e2e.components.Alert.alertV2('success').should('exist');
|
||||
} else {
|
||||
e2e.components.Alert.alert('success').should('exist');
|
||||
}
|
||||
};
|
@ -1,192 +0,0 @@
|
||||
import { e2e } from '..';
|
||||
import { getScenarioContext } from '../support/scenarioContext';
|
||||
|
||||
import { setDashboardTimeRange } from './setDashboardTimeRange';
|
||||
import { TimeRangeConfig } from './setTimeRange';
|
||||
|
||||
interface AddPanelOverrides {
|
||||
dataSourceName: string;
|
||||
queriesForm: (config: AddPanelConfig) => void;
|
||||
panelTitle: string;
|
||||
}
|
||||
|
||||
interface EditPanelOverrides {
|
||||
queriesForm?: (config: EditPanelConfig) => void;
|
||||
panelTitle: string;
|
||||
}
|
||||
|
||||
interface ConfigurePanelDefault {
|
||||
chartData: {
|
||||
method: string;
|
||||
route: string | RegExp;
|
||||
};
|
||||
dashboardUid: string;
|
||||
matchScreenshot: boolean;
|
||||
saveDashboard: boolean;
|
||||
screenshotName: string;
|
||||
visitDashboardAtStart: boolean; // @todo remove when possible
|
||||
}
|
||||
|
||||
interface ConfigurePanelOptional {
|
||||
dataSourceName?: string;
|
||||
queriesForm?: (config: ConfigurePanelConfig) => void;
|
||||
panelTitle?: string;
|
||||
timeRange?: TimeRangeConfig;
|
||||
visualizationName?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ConfigurePanelRequired {
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
export type PartialConfigurePanelConfig = Partial<ConfigurePanelDefault> &
|
||||
ConfigurePanelOptional &
|
||||
ConfigurePanelRequired;
|
||||
|
||||
export type ConfigurePanelConfig = ConfigurePanelDefault & ConfigurePanelOptional & ConfigurePanelRequired;
|
||||
|
||||
export type PartialAddPanelConfig = PartialConfigurePanelConfig & AddPanelOverrides;
|
||||
export type AddPanelConfig = ConfigurePanelConfig & AddPanelOverrides;
|
||||
|
||||
export type PartialEditPanelConfig = PartialConfigurePanelConfig & EditPanelOverrides;
|
||||
export type EditPanelConfig = ConfigurePanelConfig & EditPanelOverrides;
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable<AddPanelConfig | EditPanelConfig | ConfigurePanelConfig>`
|
||||
export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelConfig | PartialConfigurePanelConfig) =>
|
||||
getScenarioContext().then(({ lastAddedDashboardUid }: any) => {
|
||||
const fullConfig: AddPanelConfig | EditPanelConfig | ConfigurePanelConfig = {
|
||||
chartData: {
|
||||
method: 'POST',
|
||||
route: '/api/ds/query',
|
||||
},
|
||||
dashboardUid: lastAddedDashboardUid,
|
||||
matchScreenshot: false,
|
||||
saveDashboard: true,
|
||||
screenshotName: 'panel-visualization',
|
||||
visitDashboardAtStart: true,
|
||||
...config,
|
||||
};
|
||||
|
||||
const {
|
||||
chartData,
|
||||
dashboardUid,
|
||||
dataSourceName,
|
||||
isEdit,
|
||||
matchScreenshot,
|
||||
panelTitle,
|
||||
queriesForm,
|
||||
screenshotName,
|
||||
timeRange,
|
||||
visitDashboardAtStart,
|
||||
visualizationName,
|
||||
timeout,
|
||||
} = fullConfig;
|
||||
|
||||
if (visitDashboardAtStart) {
|
||||
e2e.flows.openDashboard({ uid: dashboardUid });
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
e2e.components.Panels.Panel.title(panelTitle).click();
|
||||
e2e.components.Panels.Panel.headerItems('Edit').click();
|
||||
} else {
|
||||
try {
|
||||
e2e.components.PageToolbar.itemButton('Add button').should('be.visible');
|
||||
e2e.components.PageToolbar.itemButton('Add button').click();
|
||||
} catch (e) {
|
||||
// Depending on the screen size, the "Add" button might be hidden
|
||||
e2e.components.PageToolbar.item('Show more items').click();
|
||||
e2e.components.PageToolbar.item('Add button').last().click();
|
||||
}
|
||||
e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible');
|
||||
e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click();
|
||||
}
|
||||
|
||||
if (timeRange) {
|
||||
setDashboardTimeRange(timeRange);
|
||||
}
|
||||
|
||||
// @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296
|
||||
|
||||
e2e().intercept(chartData.method, chartData.route).as('chartData');
|
||||
|
||||
if (dataSourceName) {
|
||||
e2e.components.DataSourcePicker.container().click().type(`${dataSourceName}{downArrow}{enter}`);
|
||||
}
|
||||
|
||||
// @todo instead wait for '@pluginModule' if not already loaded
|
||||
e2e().wait(2000);
|
||||
|
||||
// `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point
|
||||
const changeTitle = panelTitle && !isEdit;
|
||||
|
||||
if (changeTitle || visualizationName) {
|
||||
if (changeTitle && panelTitle) {
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Panel options Title').type(`{selectall}${panelTitle}`);
|
||||
}
|
||||
|
||||
if (visualizationName) {
|
||||
e2e.components.PluginVisualization.item(visualizationName).scrollIntoView().click();
|
||||
|
||||
// @todo wait for '@pluginModule' if not a core visualization and not already loaded
|
||||
e2e().wait(2000);
|
||||
}
|
||||
} else {
|
||||
// Consistently closed
|
||||
closeOptions();
|
||||
}
|
||||
|
||||
if (queriesForm) {
|
||||
queriesForm(fullConfig);
|
||||
|
||||
// Wait for a possible complex visualization to render (or something related, as this isn't necessary on the dashboard page)
|
||||
// Can't assert that its HTML changed because a new query could produce the same results
|
||||
e2e().wait(1000);
|
||||
}
|
||||
|
||||
// @todo enable when plugins have this implemented
|
||||
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
|
||||
//e2e().wait('@chartData');
|
||||
//e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data');
|
||||
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
|
||||
//e2e().wait('@chartData');
|
||||
|
||||
// Avoid annotations flakiness
|
||||
e2e.components.RefreshPicker.runButtonV2().first().click({ force: true });
|
||||
|
||||
// Wait for RxJS
|
||||
e2e().wait(timeout ?? e2e.config().defaultCommandTimeout);
|
||||
|
||||
if (matchScreenshot) {
|
||||
let visualization;
|
||||
|
||||
visualization = e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content');
|
||||
|
||||
visualization.scrollIntoView().screenshot(screenshotName);
|
||||
e2e().compareScreenshots(screenshotName);
|
||||
}
|
||||
|
||||
// @todo remove `wrap` when possible
|
||||
return e2e().wrap({ config: fullConfig }, { log: false });
|
||||
});
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
const closeOptions = () => e2e.components.PanelEditor.toggleVizOptions().click();
|
||||
|
||||
export const VISUALIZATION_ALERT_LIST = 'Alert list';
|
||||
export const VISUALIZATION_BAR_GAUGE = 'Bar gauge';
|
||||
export const VISUALIZATION_CLOCK = 'Clock';
|
||||
export const VISUALIZATION_DASHBOARD_LIST = 'Dashboard list';
|
||||
export const VISUALIZATION_GAUGE = 'Gauge';
|
||||
export const VISUALIZATION_GRAPH = 'Graph';
|
||||
export const VISUALIZATION_HEAT_MAP = 'Heatmap';
|
||||
export const VISUALIZATION_LOGS = 'Logs';
|
||||
export const VISUALIZATION_NEWS = 'News';
|
||||
export const VISUALIZATION_PIE_CHART = 'Pie Chart';
|
||||
export const VISUALIZATION_PLUGIN_LIST = 'Plugin list';
|
||||
export const VISUALIZATION_POLYSTAT = 'Polystat';
|
||||
export const VISUALIZATION_STAT = 'Stat';
|
||||
export const VISUALIZATION_TABLE = 'Table';
|
||||
export const VISUALIZATION_TEXT = 'Text';
|
||||
export const VISUALIZATION_WORLD_MAP = 'Worldmap Panel';
|
@ -1,51 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
import { fromBaseUrl } from '../support/url';
|
||||
|
||||
export interface DeleteDashboardConfig {
|
||||
quick?: boolean;
|
||||
title: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export const deleteDashboard = ({ quick = false, title, uid }: DeleteDashboardConfig) => {
|
||||
e2e().logToConsole('Deleting dashboard with uid:', uid);
|
||||
|
||||
if (quick) {
|
||||
quickDelete(uid);
|
||||
} else {
|
||||
uiDelete(uid, title);
|
||||
}
|
||||
|
||||
e2e().logToConsole('Deleted dashboard with uid:', uid);
|
||||
|
||||
e2e.getScenarioContext().then(({ addedDashboards }: any) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDashboards: addedDashboards.filter((dashboard: DeleteDashboardConfig) => {
|
||||
return dashboard.title !== title && dashboard.uid !== uid;
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const quickDelete = (uid: string) => {
|
||||
e2e().request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}`));
|
||||
};
|
||||
|
||||
const uiDelete = (uid: string, title: string) => {
|
||||
e2e.pages.Dashboard.visit(uid);
|
||||
e2e.components.PageToolbar.item('Dashboard settings').click();
|
||||
e2e.pages.Dashboard.Settings.General.deleteDashBoard().click();
|
||||
e2e.pages.ConfirmModal.delete().click();
|
||||
e2e.flows.assertSuccessNotification();
|
||||
|
||||
e2e.pages.Dashboards.visit();
|
||||
|
||||
// @todo replace `e2e.pages.Dashboards.dashboards` with this when argument is empty
|
||||
if (e2e.components.Search.dashboardItems) {
|
||||
e2e.components.Search.dashboardItems().each((item) => e2e().wrap(item).should('not.contain', title));
|
||||
} else {
|
||||
e2e()
|
||||
.get('[aria-label^="Dashboard search item "]')
|
||||
.each((item) => e2e().wrap(item).should('not.contain', title));
|
||||
}
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
import { fromBaseUrl } from '../support/url';
|
||||
|
||||
export interface DeleteDataSourceConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
quick?: boolean;
|
||||
}
|
||||
|
||||
export const deleteDataSource = ({ id, name, quick = false }: DeleteDataSourceConfig) => {
|
||||
e2e().logToConsole('Deleting data source with name:', name);
|
||||
|
||||
if (quick) {
|
||||
quickDelete(name);
|
||||
} else {
|
||||
uiDelete(name);
|
||||
}
|
||||
|
||||
e2e().logToConsole('Deleted data source with name:', name);
|
||||
|
||||
e2e.getScenarioContext().then(({ addedDataSources }: any) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDataSources: addedDataSources.filter((dataSource: DeleteDataSourceConfig) => {
|
||||
return dataSource.id !== id && dataSource.name !== name;
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const quickDelete = (name: string) => {
|
||||
e2e().request('DELETE', fromBaseUrl(`/api/datasources/name/${name}`));
|
||||
};
|
||||
|
||||
const uiDelete = (name: string) => {
|
||||
e2e.pages.DataSources.visit();
|
||||
e2e.pages.DataSources.dataSources(name).click();
|
||||
e2e.pages.DataSource.delete().click();
|
||||
e2e.pages.ConfirmModal.delete().click();
|
||||
|
||||
e2e.pages.DataSources.visit();
|
||||
|
||||
// @todo replace `e2e.pages.DataSources.dataSources` with this when argument is empty
|
||||
e2e()
|
||||
.get('[aria-label^="Data source list item "]')
|
||||
.each((item) => e2e().wrap(item).should('not.contain', name));
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
import { configurePanel, PartialEditPanelConfig } from './configurePanel';
|
||||
|
||||
export const editPanel = (config: Partial<PartialEditPanelConfig>) =>
|
||||
configurePanel({
|
||||
...config,
|
||||
isEdit: true,
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
import { fromBaseUrl, getDashboardUid } from '../support/url';
|
||||
|
||||
import { DeleteDashboardConfig } from '.';
|
||||
|
||||
type Panel = {
|
||||
title: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* Smoke test a particular dashboard by quickly importing a json file and validate that all the panels finish loading
|
||||
* @param dashboardToImport a sample dashboard
|
||||
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
|
||||
* @param skipPanelValidation skip panel validation
|
||||
*/
|
||||
export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: number, skipPanelValidation?: boolean) => {
|
||||
e2e().visit(fromBaseUrl('/dashboard/import'));
|
||||
|
||||
// Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster
|
||||
e2e.components.DashboardImportPage.textarea().should('be.visible');
|
||||
e2e.components.DashboardImportPage.textarea().click();
|
||||
e2e.components.DashboardImportPage.textarea().invoke('val', JSON.stringify(dashboardToImport));
|
||||
e2e.components.DashboardImportPage.submit().should('be.visible').click();
|
||||
e2e.components.ImportDashboardForm.name().should('be.visible').click().clear().type(dashboardToImport.title);
|
||||
e2e.components.ImportDashboardForm.submit().should('be.visible').click();
|
||||
|
||||
// wait for dashboard to load
|
||||
e2e().wait(queryTimeout || 6000);
|
||||
|
||||
// save the newly imported dashboard to context so it'll get properly deleted later
|
||||
e2e()
|
||||
.url()
|
||||
.should('contain', '/d/')
|
||||
.then((url: string) => {
|
||||
const uid = getDashboardUid(url);
|
||||
|
||||
e2e.getScenarioContext().then(({ addedDashboards }: { addedDashboards: DeleteDashboardConfig[] }) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDashboards: [...addedDashboards, { title: dashboardToImport.title, uid }],
|
||||
});
|
||||
});
|
||||
|
||||
expect(dashboardToImport.uid).to.equal(uid);
|
||||
});
|
||||
|
||||
if (!skipPanelValidation) {
|
||||
dashboardToImport.panels.forEach((panel) => {
|
||||
// Look at the json data
|
||||
e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover
|
||||
e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click();
|
||||
e2e.components.Tab.title('JSON').should('be.visible').click();
|
||||
e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true });
|
||||
e2e.components.Select.option().should('be.visible').contains('Panel data').click();
|
||||
|
||||
// ensures that panel has loaded without knowingly hitting an error
|
||||
// note: this does not prove that data came back as we expected it,
|
||||
// it could get `state: Done` for no data for example
|
||||
// but it ensures we didn't hit a 401 or 500 or something like that
|
||||
e2e.components.CodeEditor.container()
|
||||
.should('be.visible')
|
||||
.contains(/"state": "(Done|Streaming)"/);
|
||||
|
||||
// need to close panel
|
||||
e2e.components.Drawer.General.close().click();
|
||||
});
|
||||
}
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
import { importDashboard, Dashboard } from './importDashboard';
|
||||
|
||||
/**
|
||||
* Smoke test several dashboard json files from a test directory
|
||||
* and validate that all the panels in each import finish loading their queries
|
||||
* @param dirPath the relative path to a directory which contains json files representing dashboards,
|
||||
* for example if your dashboards live in `cypress/testDashboards` you can pass `/testDashboards`
|
||||
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
|
||||
* @param skipPanelValidation skips panel validation
|
||||
*/
|
||||
export const importDashboards = async (dirPath: string, queryTimeout?: number, skipPanelValidation?: boolean) => {
|
||||
e2e()
|
||||
.getJSONFilesFromDir(dirPath)
|
||||
.then((jsonFiles: Dashboard[]) => {
|
||||
jsonFiles.forEach((file) => {
|
||||
importDashboard(file, queryTimeout || 6000, skipPanelValidation);
|
||||
});
|
||||
});
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
export * from './addDashboard';
|
||||
export * from './addDataSource';
|
||||
export * from './addPanel';
|
||||
export * from './assertSuccessNotification';
|
||||
export * from './deleteDashboard';
|
||||
export * from './deleteDataSource';
|
||||
export * from './editPanel';
|
||||
export * from './login';
|
||||
export * from './openDashboard';
|
||||
export * from './openPanelMenuItem';
|
||||
export * from './revertAllChanges';
|
||||
export * from './saveDashboard';
|
||||
export * from './selectOption';
|
||||
export * from './setTimeRange';
|
||||
export * from './importDashboard';
|
||||
export * from './importDashboards';
|
||||
export * from './userPreferences';
|
||||
|
||||
export {
|
||||
VISUALIZATION_ALERT_LIST,
|
||||
VISUALIZATION_BAR_GAUGE,
|
||||
VISUALIZATION_CLOCK,
|
||||
VISUALIZATION_DASHBOARD_LIST,
|
||||
VISUALIZATION_GAUGE,
|
||||
VISUALIZATION_GRAPH,
|
||||
VISUALIZATION_HEAT_MAP,
|
||||
VISUALIZATION_LOGS,
|
||||
VISUALIZATION_NEWS,
|
||||
VISUALIZATION_PIE_CHART,
|
||||
VISUALIZATION_PLUGIN_LIST,
|
||||
VISUALIZATION_POLYSTAT,
|
||||
VISUALIZATION_STAT,
|
||||
VISUALIZATION_TABLE,
|
||||
VISUALIZATION_TEXT,
|
||||
VISUALIZATION_WORLD_MAP,
|
||||
} from './configurePanel';
|
@ -1,42 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
import { fromBaseUrl } from '../support/url';
|
||||
|
||||
const DEFAULT_USERNAME = 'admin';
|
||||
const DEFAULT_PASSWORD = 'admin';
|
||||
|
||||
const loginApi = (username: string, password: string) => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: fromBaseUrl('/login'),
|
||||
body: {
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const loginUi = (username: string, password: string) => {
|
||||
e2e().logToConsole('Logging in with username:', username);
|
||||
e2e.pages.Login.visit();
|
||||
e2e.pages.Login.username()
|
||||
.should('be.visible') // prevents flakiness
|
||||
.type(username);
|
||||
e2e.pages.Login.password().type(password);
|
||||
e2e.pages.Login.submit().click();
|
||||
|
||||
// Local tests will have insecure credentials
|
||||
if (password === DEFAULT_PASSWORD) {
|
||||
e2e.pages.Login.skip().should('be.visible').click();
|
||||
}
|
||||
|
||||
e2e().get('.login-page').should('not.exist');
|
||||
};
|
||||
|
||||
export const login = (username = DEFAULT_USERNAME, password = DEFAULT_PASSWORD, loginViaApi = true) => {
|
||||
if (loginViaApi) {
|
||||
loginApi(username, password);
|
||||
} else {
|
||||
loginUi(username, password);
|
||||
}
|
||||
e2e().logToConsole('Logged in with username:', username);
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
import { getScenarioContext } from '../support/scenarioContext';
|
||||
|
||||
import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange';
|
||||
|
||||
interface OpenDashboardDefault {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
interface OpenDashboardOptional {
|
||||
timeRange?: TimeRangeConfig;
|
||||
queryParams?: object;
|
||||
}
|
||||
|
||||
export type PartialOpenDashboardConfig = Partial<OpenDashboardDefault> & OpenDashboardOptional;
|
||||
export type OpenDashboardConfig = OpenDashboardDefault & OpenDashboardOptional;
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable<OpenDashboardConfig>`
|
||||
export const openDashboard = (config?: PartialOpenDashboardConfig) =>
|
||||
getScenarioContext().then(({ lastAddedDashboardUid }: any) => {
|
||||
const fullConfig: OpenDashboardConfig = {
|
||||
uid: lastAddedDashboardUid,
|
||||
...config,
|
||||
};
|
||||
|
||||
const { timeRange, uid, queryParams } = fullConfig;
|
||||
|
||||
e2e.pages.Dashboard.visit(uid, queryParams);
|
||||
|
||||
if (timeRange) {
|
||||
setDashboardTimeRange(timeRange);
|
||||
}
|
||||
|
||||
// @todo remove `wrap` when possible
|
||||
return e2e().wrap({ config: fullConfig }, { log: false });
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export enum PanelMenuItems {
|
||||
Edit = 'Edit',
|
||||
Inspect = 'Inspect',
|
||||
More = 'More...',
|
||||
Extensions = 'Extensions',
|
||||
}
|
||||
|
||||
export const openPanelMenuItem = (menu: PanelMenuItems, panelTitle = 'Panel Title') => {
|
||||
// we changed the way we open the panel menu in react panels with the new panel header
|
||||
detectPanelType(panelTitle, (isAngularPanel) => {
|
||||
if (isAngularPanel) {
|
||||
e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click();
|
||||
e2e.components.Panels.Panel.headerItems(menu).should('be.visible').click();
|
||||
} else {
|
||||
e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover
|
||||
e2e.components.Panels.Panel.menuItems(menu).should('be.visible').click();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const openPanelMenuExtension = (extensionTitle: string, panelTitle = 'Panel Title') => {
|
||||
const menuItem = PanelMenuItems.Extensions;
|
||||
// we changed the way we open the panel menu in react panels with the new panel header
|
||||
detectPanelType(panelTitle, (isAngularPanel) => {
|
||||
if (isAngularPanel) {
|
||||
e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click();
|
||||
e2e.components.Panels.Panel.headerItems(menuItem)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.parent()
|
||||
.invoke('addClass', 'open');
|
||||
e2e.components.Panels.Panel.headerItems(extensionTitle).should('be.visible').click();
|
||||
} else {
|
||||
e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover
|
||||
e2e.components.Panels.Panel.menuItems(menuItem).trigger('mouseover', { force: true });
|
||||
e2e.components.Panels.Panel.menuItems(extensionTitle).click({ force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function detectPanelType(panelTitle: string, detected: (isAngularPanel: boolean) => void) {
|
||||
e2e.components.Panels.Panel.title(panelTitle).then((el) => {
|
||||
const isAngularPanel = el.find('plugin-component.ng-scope').length > 0;
|
||||
|
||||
if (isAngularPanel) {
|
||||
Cypress.log({
|
||||
name: 'detectPanelType',
|
||||
displayName: 'detector',
|
||||
message: 'Angular panel detected, will use legacy selectors.',
|
||||
});
|
||||
}
|
||||
|
||||
detected(isAngularPanel);
|
||||
});
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const revertAllChanges = () => {
|
||||
e2e.getScenarioContext().then(({ addedDashboards, addedDataSources, hasChangedUserPreferences }) => {
|
||||
addedDashboards.forEach((dashboard: any) => e2e.flows.deleteDashboard({ ...dashboard, quick: true }));
|
||||
addedDataSources.forEach((dataSource: any) => e2e.flows.deleteDataSource({ ...dataSource, quick: true }));
|
||||
|
||||
if (hasChangedUserPreferences) {
|
||||
e2e.flows.setDefaultUserPreferences();
|
||||
}
|
||||
});
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const saveDashboard = () => {
|
||||
e2e.components.PageToolbar.item('Save dashboard').click();
|
||||
|
||||
e2e.pages.SaveDashboardModal.save().click();
|
||||
|
||||
e2e.flows.assertSuccessNotification();
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export interface SelectOptionConfig {
|
||||
clickToOpen?: boolean;
|
||||
container: any;
|
||||
forceClickOption?: boolean;
|
||||
optionText: string | RegExp;
|
||||
}
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
export const selectOption = (config: SelectOptionConfig): any => {
|
||||
const fullConfig: SelectOptionConfig = {
|
||||
clickToOpen: true,
|
||||
forceClickOption: false,
|
||||
...config,
|
||||
};
|
||||
|
||||
const { clickToOpen, container, forceClickOption, optionText } = fullConfig;
|
||||
|
||||
container.within(() => {
|
||||
if (clickToOpen) {
|
||||
e2e()
|
||||
.get('[class$="-input-suffix"]', { timeout: 1000 })
|
||||
.then((element) => {
|
||||
expect(Cypress.dom.isAttached(element)).to.eq(true);
|
||||
e2e().get('[class$="-input-suffix"]', { timeout: 1000 }).click({ force: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return e2e.components.Select.option()
|
||||
.filter((_, { textContent }) => {
|
||||
if (textContent === null) {
|
||||
return false;
|
||||
} else if (typeof optionText === 'string') {
|
||||
return textContent.includes(optionText);
|
||||
} else {
|
||||
return optionText.test(textContent);
|
||||
}
|
||||
})
|
||||
.scrollIntoView()
|
||||
.click({ force: forceClickOption });
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import { setTimeRange, TimeRangeConfig } from './setTimeRange';
|
||||
|
||||
export type { TimeRangeConfig };
|
||||
|
||||
export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config);
|
@ -1,40 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
import { selectOption } from './selectOption';
|
||||
|
||||
export interface TimeRangeConfig {
|
||||
from: string;
|
||||
to: string;
|
||||
zone?: string;
|
||||
}
|
||||
|
||||
export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
||||
e2e.components.TimePicker.openButton().click();
|
||||
|
||||
if (zone) {
|
||||
e2e().contains('button', 'Change time settings').click();
|
||||
e2e().log('setting time zone to ' + zone);
|
||||
|
||||
if (e2e.components.TimeZonePicker.containerV2) {
|
||||
selectOption({
|
||||
clickToOpen: true,
|
||||
container: e2e.components.TimeZonePicker.containerV2(),
|
||||
optionText: zone,
|
||||
});
|
||||
} else {
|
||||
selectOption({
|
||||
clickToOpen: true,
|
||||
container: e2e.components.TimeZonePicker.container(),
|
||||
optionText: zone,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// For smaller screens
|
||||
e2e.components.TimePicker.absoluteTimeRangeTitle().click();
|
||||
|
||||
e2e.components.TimePicker.fromField().clear().type(from);
|
||||
e2e.components.TimePicker.toField().clear().type(to);
|
||||
|
||||
e2e.components.TimePicker.applyTimeRange().click();
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
|
||||
|
||||
import { e2e } from '..';
|
||||
import { fromBaseUrl } from '../support/url';
|
||||
|
||||
const defaultUserPreferences = {
|
||||
timezone: '', // "Default" option
|
||||
} as const; // TODO: when we update typescript >4.9 change to `as const satisfies UserPreferencesDTO`
|
||||
|
||||
// Only accept preferences we have defaults for as arguments. To allow a new preference to be set, add a default for it
|
||||
type UserPreferences = Pick<UserPreferencesDTO, keyof typeof defaultUserPreferences>;
|
||||
|
||||
export function setUserPreferences(prefs: UserPreferences) {
|
||||
e2e.setScenarioContext({ hasChangedUserPreferences: prefs !== defaultUserPreferences });
|
||||
|
||||
return cy.request({
|
||||
method: 'PUT',
|
||||
url: fromBaseUrl('/api/user/preferences'),
|
||||
body: prefs,
|
||||
});
|
||||
}
|
||||
|
||||
export function setDefaultUserPreferences() {
|
||||
return setUserPreferences(defaultUserPreferences);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* A library for writing end-to-end tests for Grafana and its ecosystem.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import * as flows from './flows';
|
||||
import { e2eFactory } from './support';
|
||||
import { benchmark } from './support/benchmark';
|
||||
import { e2eScenario, ScenarioArguments } from './support/scenario';
|
||||
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
|
||||
import * as typings from './typings';
|
||||
|
||||
const e2eObject = {
|
||||
env: (args: string) => Cypress.env(args),
|
||||
config: () => Cypress.config(),
|
||||
blobToBase64String: (blob: Blob) => Cypress.Blob.blobToBase64String(blob),
|
||||
imgSrcToBlob: (url: string) => Cypress.Blob.imgSrcToBlob(url),
|
||||
scenario: (args: ScenarioArguments) => e2eScenario(args),
|
||||
benchmark,
|
||||
pages: e2eFactory({ selectors: selectors.pages }),
|
||||
typings,
|
||||
components: e2eFactory({ selectors: selectors.components }),
|
||||
flows,
|
||||
getScenarioContext,
|
||||
setScenarioContext,
|
||||
getSelectors: <T extends Selectors>(selectors: E2ESelectors<T>) => e2eFactory({ selectors }),
|
||||
};
|
||||
|
||||
export const e2e: (() => Cypress.cy) & typeof e2eObject = Object.assign(() => cy, e2eObject);
|
@ -1,81 +0,0 @@
|
||||
import { e2e } from '../';
|
||||
|
||||
export interface BenchmarkArguments {
|
||||
name: string;
|
||||
dashboard: {
|
||||
folder: string;
|
||||
delayAfterOpening: number;
|
||||
skipPanelValidation: boolean;
|
||||
};
|
||||
repeat: number;
|
||||
duration: number;
|
||||
appStats?: {
|
||||
startCollecting?: (window: Window) => void;
|
||||
collect: (window: Window) => Record<string, unknown>;
|
||||
};
|
||||
skipScenario?: boolean;
|
||||
}
|
||||
|
||||
export const benchmark = ({
|
||||
name,
|
||||
skipScenario = false,
|
||||
repeat,
|
||||
duration,
|
||||
appStats,
|
||||
dashboard,
|
||||
}: BenchmarkArguments) => {
|
||||
if (skipScenario) {
|
||||
describe(name, () => {
|
||||
it.skip(name, () => {});
|
||||
});
|
||||
}
|
||||
|
||||
describe(name, () => {
|
||||
before(() => {
|
||||
e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
e2e.flows.importDashboards(dashboard.folder, 1000, dashboard.skipPanelValidation);
|
||||
Cypress.Cookies.preserveOnce('grafana_session');
|
||||
});
|
||||
|
||||
afterEach(() => e2e.flows.revertAllChanges());
|
||||
after(() => {
|
||||
e2e().clearCookies();
|
||||
});
|
||||
|
||||
Array(repeat)
|
||||
.fill(0)
|
||||
.map((_, i) => {
|
||||
const testName = `${name}-${i}`;
|
||||
return it(testName, () => {
|
||||
e2e.flows.openDashboard();
|
||||
|
||||
e2e().wait(dashboard.delayAfterOpening);
|
||||
|
||||
if (appStats) {
|
||||
const startCollecting = appStats.startCollecting;
|
||||
if (startCollecting) {
|
||||
e2e()
|
||||
.window()
|
||||
.then((win) => startCollecting(win));
|
||||
}
|
||||
|
||||
e2e().startBenchmarking(testName);
|
||||
e2e().wait(duration);
|
||||
|
||||
e2e()
|
||||
.window()
|
||||
.then((win) => {
|
||||
e2e().stopBenchmarking(testName, appStats.collect(win));
|
||||
});
|
||||
} else {
|
||||
e2e().startBenchmarking(testName);
|
||||
e2e().wait(duration);
|
||||
e2e().stopBenchmarking(testName, {});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
export * from './localStorage';
|
||||
export * from './scenarioContext';
|
||||
export * from './selector';
|
||||
export * from './types';
|
@ -1,23 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
const get = (key: string): any =>
|
||||
e2e()
|
||||
.wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false })
|
||||
.invoke('getLocalStorage');
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
export const getLocalStorage = (key: string): any =>
|
||||
get(key).then((value: any) => {
|
||||
if (value === null) {
|
||||
return value;
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
});
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
export const requireLocalStorage = (key: string): any =>
|
||||
get(key) // `getLocalStorage()` would turn 'null' into `null`
|
||||
.should('not.equal', null)
|
||||
.then((value: any) => JSON.parse(value as string));
|
@ -1,51 +0,0 @@
|
||||
import { e2e } from '../';
|
||||
|
||||
export interface ScenarioArguments {
|
||||
describeName: string;
|
||||
itName: string;
|
||||
scenario: Function;
|
||||
skipScenario?: boolean;
|
||||
addScenarioDataSource?: boolean;
|
||||
addScenarioDashBoard?: boolean;
|
||||
loginViaApi?: boolean;
|
||||
}
|
||||
|
||||
export const e2eScenario = ({
|
||||
describeName,
|
||||
itName,
|
||||
scenario,
|
||||
skipScenario = false,
|
||||
addScenarioDataSource = false,
|
||||
addScenarioDashBoard = false,
|
||||
loginViaApi = true,
|
||||
}: ScenarioArguments) => {
|
||||
describe(describeName, () => {
|
||||
if (skipScenario) {
|
||||
it.skip(itName, () => scenario());
|
||||
} else {
|
||||
before(() => {
|
||||
e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), loginViaApi);
|
||||
e2e.flows.setDefaultUserPreferences();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
Cypress.Cookies.preserveOnce('grafana_session');
|
||||
|
||||
if (addScenarioDataSource) {
|
||||
e2e.flows.addDataSource();
|
||||
}
|
||||
if (addScenarioDashBoard) {
|
||||
e2e.flows.addDashboard();
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => e2e.flows.revertAllChanges());
|
||||
after(() => e2e().clearCookies());
|
||||
|
||||
it(itName, () => scenario());
|
||||
|
||||
// @todo remove when possible: https://github.com/cypress-io/cypress/issues/2831
|
||||
it('temporary', () => {});
|
||||
}
|
||||
});
|
||||
};
|
@ -1,61 +0,0 @@
|
||||
import { DeleteDashboardConfig } from '../flows/deleteDashboard';
|
||||
import { DeleteDataSourceConfig } from '../flows/deleteDataSource';
|
||||
import { e2e } from '../index';
|
||||
|
||||
export interface ScenarioContext {
|
||||
addedDashboards: DeleteDashboardConfig[];
|
||||
addedDataSources: DeleteDataSourceConfig[];
|
||||
lastAddedDashboard: string; // @todo rename to `lastAddedDashboardTitle`
|
||||
lastAddedDashboardUid: string;
|
||||
lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName`
|
||||
lastAddedDataSourceId: string;
|
||||
hasChangedUserPreferences: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const scenarioContext: ScenarioContext = {
|
||||
addedDashboards: [],
|
||||
addedDataSources: [],
|
||||
hasChangedUserPreferences: false,
|
||||
get lastAddedDashboard() {
|
||||
return lastProperty(this.addedDashboards, 'title');
|
||||
},
|
||||
get lastAddedDashboardUid() {
|
||||
return lastProperty(this.addedDashboards, 'uid');
|
||||
},
|
||||
get lastAddedDataSource() {
|
||||
return lastProperty(this.addedDataSources, 'name');
|
||||
},
|
||||
get lastAddedDataSourceId() {
|
||||
return lastProperty(this.addedDataSources, 'id');
|
||||
},
|
||||
};
|
||||
|
||||
const lastProperty = <T extends DeleteDashboardConfig | DeleteDataSourceConfig, K extends keyof T>(
|
||||
items: T[],
|
||||
key: K
|
||||
) => items[items.length - 1]?.[key] ?? '';
|
||||
|
||||
export const getScenarioContext = (): Cypress.Chainable<ScenarioContext> =>
|
||||
e2e()
|
||||
.wrap(
|
||||
{
|
||||
getScenarioContext: (): ScenarioContext => ({ ...scenarioContext }),
|
||||
},
|
||||
{ log: false }
|
||||
)
|
||||
.invoke({ log: false }, 'getScenarioContext');
|
||||
|
||||
export const setScenarioContext = (newContext: Partial<ScenarioContext>): Cypress.Chainable<ScenarioContext> =>
|
||||
e2e()
|
||||
.wrap(
|
||||
{
|
||||
setScenarioContext: () => {
|
||||
Object.entries(newContext).forEach(([key, value]) => {
|
||||
scenarioContext[key] = value;
|
||||
});
|
||||
},
|
||||
},
|
||||
{ log: false }
|
||||
)
|
||||
.invoke({ log: false }, 'setScenarioContext');
|
@ -1,11 +0,0 @@
|
||||
export interface SelectorApi {
|
||||
fromAriaLabel: (selector: string) => string;
|
||||
fromDataTestId: (selector: string) => string;
|
||||
fromSelector: (selector: string) => string;
|
||||
}
|
||||
|
||||
export const Selector: SelectorApi = {
|
||||
fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`,
|
||||
fromDataTestId: (selector: string) => `[data-testid="${selector}"]`,
|
||||
fromSelector: (selector: string) => selector,
|
||||
};
|
@ -1,138 +0,0 @@
|
||||
import { CssSelector, FunctionSelector, Selectors, StringSelector, UrlSelector } from '@grafana/e2e-selectors';
|
||||
|
||||
import { e2e } from '../index';
|
||||
|
||||
import { Selector } from './selector';
|
||||
import { fromBaseUrl } from './url';
|
||||
|
||||
export type VisitFunction = (args?: string, queryParams?: object) => Cypress.Chainable<Window>;
|
||||
export type E2EVisit = { visit: VisitFunction };
|
||||
export type E2EFunction = ((text?: string, options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>) &
|
||||
E2EFunctionWithOnlyOptions;
|
||||
export type E2EFunctionWithOnlyOptions = (options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
export type TypeSelectors<S> = S extends StringSelector
|
||||
? E2EFunctionWithOnlyOptions
|
||||
: S extends FunctionSelector
|
||||
? E2EFunction
|
||||
: S extends CssSelector
|
||||
? E2EFunction
|
||||
: S extends UrlSelector
|
||||
? E2EVisit & Omit<E2EFunctions<S>, 'url'>
|
||||
: S extends Record<any, any>
|
||||
? E2EFunctions<S>
|
||||
: S;
|
||||
|
||||
export type E2EFunctions<S extends Selectors> = {
|
||||
[P in keyof S]: TypeSelectors<S[P]>;
|
||||
};
|
||||
|
||||
export type E2EObjects<S extends Selectors> = E2EFunctions<S>;
|
||||
|
||||
export type E2EFactoryArgs<S extends Selectors> = { selectors: S };
|
||||
|
||||
export type CypressOptions = Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>;
|
||||
|
||||
const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, selectors: S): E2EFunctions<S> => {
|
||||
const logOutput = (data: any) => e2e().logToConsole('Retrieving Selector:', data);
|
||||
const keys = Object.keys(selectors);
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
const key = keys[index];
|
||||
const value = selectors[key];
|
||||
|
||||
if (key === 'url') {
|
||||
// @ts-ignore
|
||||
e2eObjects['visit'] = (args?: string, queryParams?: object) => {
|
||||
let parsedUrl = '';
|
||||
if (typeof value === 'string') {
|
||||
parsedUrl = fromBaseUrl(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'function' && args) {
|
||||
parsedUrl = fromBaseUrl(value(args));
|
||||
}
|
||||
|
||||
e2e().logToConsole('Visiting', parsedUrl);
|
||||
if (queryParams) {
|
||||
return e2e().visit({ url: parsedUrl, qs: queryParams });
|
||||
} else {
|
||||
return e2e().visit(parsedUrl);
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = (options?: CypressOptions) => {
|
||||
logOutput(value);
|
||||
const selector = value.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(value)
|
||||
: Selector.fromAriaLabel(value);
|
||||
|
||||
return e2e().get(selector, options);
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'function') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = function (textOrOptions?: string | CypressOptions, options?: CypressOptions) {
|
||||
// the input can only be ()
|
||||
if (arguments.length === 0) {
|
||||
const selector = value(undefined as unknown as string);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector);
|
||||
}
|
||||
|
||||
// the input can be (text) or (options)
|
||||
if (arguments.length === 1) {
|
||||
if (typeof textOrOptions === 'string') {
|
||||
const selectorText = value(textOrOptions);
|
||||
const selector = selectorText.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector);
|
||||
}
|
||||
const selector = value(undefined as unknown as string);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector, textOrOptions);
|
||||
}
|
||||
|
||||
// the input can only be (text, options)
|
||||
if (arguments.length === 2 && typeof textOrOptions === 'string') {
|
||||
const text = textOrOptions;
|
||||
const selectorText = value(text);
|
||||
const selector = text.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector, options);
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = processSelectors({}, value);
|
||||
}
|
||||
}
|
||||
|
||||
return e2eObjects;
|
||||
};
|
||||
|
||||
export const e2eFactory = <S extends Selectors>({ selectors }: E2EFactoryArgs<S>): E2EObjects<S> => {
|
||||
const e2eObjects: E2EFunctions<S> = {} as E2EFunctions<S>;
|
||||
processSelectors(e2eObjects, selectors);
|
||||
|
||||
return { ...e2eObjects };
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
const getBaseUrl = () => e2e.env('BASE_URL') || e2e.config().baseUrl || 'http://localhost:3000';
|
||||
|
||||
export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href;
|
||||
|
||||
export const getDashboardUid = (url: string): string => {
|
||||
const matches = new URL(url).pathname.match(/\/d\/([^/]+)/);
|
||||
if (!matches) {
|
||||
throw new Error(`Couldn't parse uid from ${url}`);
|
||||
} else {
|
||||
return matches[1];
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { undo } from './undo';
|
@ -1,19 +0,0 @@
|
||||
// https://nodejs.org/api/os.html#os_os_platform
|
||||
enum Platform {
|
||||
osx = 'darwin',
|
||||
windows = 'win32',
|
||||
linux = 'linux',
|
||||
aix = 'aix',
|
||||
freebsd = 'freebsd',
|
||||
openbsd = 'openbsd',
|
||||
sunos = 'sunos',
|
||||
}
|
||||
|
||||
export const undo = () => {
|
||||
switch (Cypress.platform) {
|
||||
case Platform.osx:
|
||||
return '{cmd}z';
|
||||
default:
|
||||
return '{ctrl}z';
|
||||
}
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
describe('CLI', () => {
|
||||
it('compiles this file and runs it', () => {});
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
import { e2e } from '../../../dist';
|
||||
|
||||
describe('API', () => {
|
||||
it('can be imported', () => {
|
||||
expect(e2e).to.be.a('function');
|
||||
});
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../node_modules",
|
||||
"types": ["cypress", "cypress-file-upload"]
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declarationDir": "./compiled",
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["src/**/*.ts", "cypress/support/index.d.ts"]
|
||||
}
|
@ -4,24 +4,24 @@
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": ["@grafana/runtime", "@grafana/data/*", "@grafana/ui", "@grafana/e2e", "@grafana/e2e-selectors/*"],
|
||||
"patterns": ["@grafana/runtime", "@grafana/data/*", "@grafana/ui", "@grafana/e2e-selectors/*"],
|
||||
"paths": [
|
||||
{
|
||||
"name": "react-i18next",
|
||||
"importNames": ["Trans", "t"],
|
||||
"message": "Please import from grafana-ui/src/utils/i18n instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"message": "Please import from grafana-ui/src/utils/i18n instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.{test,story}.{ts,tsx}"],
|
||||
"rules": {
|
||||
"no-restricted-imports": "off",
|
||||
"react/prop-types": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ var packages = []string{
|
||||
"@grafana/ui",
|
||||
"@grafana/data",
|
||||
"@grafana/runtime",
|
||||
"@grafana/e2e",
|
||||
"@grafana/e2e-selectors",
|
||||
"@grafana/schema",
|
||||
"@grafana/flamegraph",
|
||||
|
Loading…
Reference in New Issue
Block a user