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