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:
Erik Sundell 2024-04-25 07:12:43 +02:00 committed by GitHub
parent f1aa6549f6
commit a3ef463499
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 197 additions and 13031 deletions

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

@ -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": [

View File

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

View File

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

View File

@ -1,3 +0,0 @@
test/cypress/report.json
test/cypress/screenshots/actual
test/cypress/videos/

View File

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

View File

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

View File

@ -1,3 +0,0 @@
#!/usr/bin/env node
require('../cli')();

View File

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

View File

@ -1,7 +0,0 @@
{
"projectId": "zb7k1c",
"supportFile": "cypress/support/index.ts",
"videoCompression": 20,
"viewportWidth": 1920,
"viewportHeight": 1080
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"declaration": false,
"module": "commonjs",
"types": ["cypress", "cypress-file-upload", "node"]
},
"extends": "@grafana/tsconfig",
"include": ["**/*.ts"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import { configurePanel, PartialEditPanelConfig } from './configurePanel';
export const editPanel = (config: Partial<PartialEditPanelConfig>) =>
configurePanel({
...config,
isEdit: true,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import { setTimeRange, TimeRangeConfig } from './setTimeRange';
export type { TimeRangeConfig };
export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
export * from './localStorage';
export * from './scenarioContext';
export * from './selector';
export * from './types';

View File

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

View File

@ -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', () => {});
}
});
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
export { undo } from './undo';

View File

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

View File

@ -1,3 +0,0 @@
describe('CLI', () => {
it('compiles this file and runs it', () => {});
});

View File

@ -1,7 +0,0 @@
import { e2e } from '../../../dist';
describe('API', () => {
it('can be imported', () => {
expect(e2e).to.be.a('function');
});
});

View File

@ -1,8 +0,0 @@
{
"extends": "@grafana/tsconfig",
"include": ["**/*.ts"],
"compilerOptions": {
"baseUrl": "../node_modules",
"types": ["cypress", "cypress-file-upload"]
}
}

View File

@ -1,4 +0,0 @@
{
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
"extends": "./tsconfig.json"
}

View File

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

View File

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

View File

@ -22,7 +22,6 @@ var packages = []string{
"@grafana/ui",
"@grafana/data",
"@grafana/runtime",
"@grafana/e2e",
"@grafana/e2e-selectors",
"@grafana/schema",
"@grafana/flamegraph",

1173
yarn.lock

File diff suppressed because it is too large Load Diff