@globisnv/cypress-core 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -11
- package/biome-plugins/noUnnecessaryWaiting.grit +32 -0
- package/biome.json +2 -14
- package/dist/blueprints/GLO_departureScheduleTimes.js +3 -3
- package/dist/blueprints/GLO_enterCountry.d.ts +1 -0
- package/dist/blueprints/GLO_enterCountry.js +1 -0
- package/dist/blueprints/GLO_postalcode.d.ts +1 -0
- package/dist/blueprints/GLO_postalcode.js +7 -1
- package/dist/commands/FOB_commands.js +2 -2
- package/dist/commands/GLO_commands.js +93 -41
- package/dist/commands/POR_commands.js +1 -1
- package/dist/websocketDetector.js +1 -1
- package/package.json +3 -3
- package/template/biome.json +2 -1
- package/template/package.json +3 -3
- package/scripts/check-unnecessary-waiting.mjs +0 -119
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Core building blocks for Globis Cypress E2E tests.
|
|
4
4
|
|
|
5
|
-
This package holds everything that is *not* customer-specific: the shared Cypress config factory, the custom command library for the Servoy-based Globis solutions, the WebSocket-aware loading detector, the reusable data-entry blueprints, and the lint
|
|
5
|
+
This package holds everything that is *not* customer-specific: the shared Cypress config factory, the custom command library for the Servoy-based Globis solutions, the WebSocket-aware loading detector, the reusable data-entry blueprints, and the lint plugins. Customer repos install it, call `defineGlobisConfig()` in their `cypress.config.ts`, import `@globisnv/cypress-core/register` in their support file, and only keep their own specs and customer-specific commands locally.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -165,7 +165,7 @@ src/
|
|
|
165
165
|
├── commands/ # 6 generic command files (~128 custom commands)
|
|
166
166
|
├── apiMethods/ # IGS REST request/response helpers
|
|
167
167
|
└── plugins/ # prometheusWriter — SigV4-signed remote write to AWS AMP
|
|
168
|
-
|
|
168
|
+
biome-plugins/ # GritQL lint rules (shipped to consumers)
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
### Command families
|
|
@@ -242,23 +242,22 @@ npm run lint # biome check .
|
|
|
242
242
|
npm run lint:fix # biome check . --write
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
Formatting is Biome ([biome.json](biome.json)): 4-space indent, single quotes, semicolons, no trailing commas, 180-column lines, LF endings.
|
|
245
|
+
Formatting is Biome ([biome.json](biome.json)): 4-space indent, single quotes, semicolons, no trailing commas, 180-column lines, LF endings. [src/biome.json](src/biome.json) extends it for `src/` only: it enables `noUnnecessaryWaiting` there and disables `noUnusedVariables` for `types.ts`, which is declaration-only. It lives in `src/` because consumers `extends` the root `biome.json`, and a plugin path in there would resolve against their repo.
|
|
246
246
|
|
|
247
|
-
### Shipped lint
|
|
247
|
+
### Shipped lint plugins
|
|
248
248
|
|
|
249
|
-
`
|
|
249
|
+
`biome-plugins/` is published with the package so consumer repos run the same rules over their specs as Biome [GritQL plugins](https://biomejs.dev/linter/plugins/). Their diagnostics show up in `biome check`, the editor and the CI linter like any built-in rule.
|
|
250
250
|
|
|
251
|
-
|
|
|
251
|
+
| Plugin | What it does |
|
|
252
252
|
| --- | --- |
|
|
253
|
-
| [
|
|
253
|
+
| [noUnnecessaryWaiting.grit](biome-plugins/noUnnecessaryWaiting.grit) | Fails on `cy.wait(<number>)`, including numeric constants/default params declared in an enclosing scope. Exempt a call with `// biome-ignore lint/plugin/noUnnecessaryWaiting: <reason>` on the line above. |
|
|
254
254
|
|
|
255
|
-
Wire it into the consumer repo
|
|
255
|
+
Wire it into the consumer repo's `biome.json` (the template already does). Biome resolves plugin paths from the consumer's root, not from an `extends`-ed config, so the path can't live in this package's `biome.json`:
|
|
256
256
|
|
|
257
257
|
```json
|
|
258
258
|
{
|
|
259
|
-
"
|
|
260
|
-
|
|
261
|
-
}
|
|
259
|
+
"extends": ["./node_modules/@globisnv/cypress-core/biome.json"],
|
|
260
|
+
"plugins": ["./node_modules/@globisnv/cypress-core/biome-plugins/noUnnecessaryWaiting.grit"]
|
|
262
261
|
}
|
|
263
262
|
```
|
|
264
263
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
// A numeric `const`/`let`/`var` declared directly in this list of statements.
|
|
4
|
+
pattern numeric_decl($name) {
|
|
5
|
+
some JsVariableStatement(declaration=JsVariableDeclaration(declarators=some JsVariableDeclarator(id=$name, initializer=JsInitializerClause(expression=JsNumberLiteralExpression()))))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// A parameter with a numeric default in this parameter list.
|
|
9
|
+
pattern numeric_param($name) { JsParameters(items=some JsFormalParameter(binding=$name, initializer=JsInitializerClause(expression=JsNumberLiteralExpression()))) }
|
|
10
|
+
|
|
11
|
+
// Only a scope's own statements/parameters count, so a same-named local in another function doesn't match.
|
|
12
|
+
`cy.wait($ms, $...)` as $call where {
|
|
13
|
+
or {
|
|
14
|
+
$ms <: JsNumberLiteralExpression(),
|
|
15
|
+
and {
|
|
16
|
+
$ms <: JsIdentifierExpression(),
|
|
17
|
+
$ms <: r"(.*)"($name),
|
|
18
|
+
$call <: within or {
|
|
19
|
+
JsFunctionBody(statements=numeric_decl($name)),
|
|
20
|
+
JsBlockStatement(statements=numeric_decl($name)),
|
|
21
|
+
JsModule(items=numeric_decl($name)),
|
|
22
|
+
JsScript(statements=numeric_decl($name)),
|
|
23
|
+
JsFunctionDeclaration(parameters=numeric_param($name)),
|
|
24
|
+
JsFunctionExpression(parameters=numeric_param($name)),
|
|
25
|
+
JsArrowFunctionExpression(parameters=numeric_param($name)),
|
|
26
|
+
JsMethodClassMember(parameters=numeric_param($name)),
|
|
27
|
+
JsMethodObjectMember(parameters=numeric_param($name))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
register_diagnostic(span=$call, message="Avoid arbitrary cy.wait(ms); prefer cy.intercept()/should() polling instead.")
|
|
32
|
+
}
|
package/biome.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.5.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
|
|
3
3
|
"vcs": {
|
|
4
4
|
"enabled": true,
|
|
5
5
|
"clientKind": "git",
|
|
@@ -44,17 +44,5 @@
|
|
|
44
44
|
"noDoubleEquals": "error"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
}
|
|
48
|
-
"overrides": [
|
|
49
|
-
{
|
|
50
|
-
"includes": ["src/types.ts"],
|
|
51
|
-
"linter": {
|
|
52
|
-
"rules": {
|
|
53
|
-
"correctness": {
|
|
54
|
-
"noUnusedVariables": "off"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
]
|
|
47
|
+
}
|
|
60
48
|
}
|
|
@@ -3,11 +3,11 @@ const formName = 'ng$logistic_conn_schedule_times_dtl';
|
|
|
3
3
|
const addressFormName = 'ng$remark_dtl_logistic_conn_schedule_times';
|
|
4
4
|
const detailTabsName = 'tabs_70';
|
|
5
5
|
export const GLO_enterDepartureScheduleTimes = (obj) => {
|
|
6
|
-
//
|
|
6
|
+
// The times are Time-Box fields; each time zone is a field of its own, shown while Use time zones is on
|
|
7
7
|
cy.GLO_addInput([formName, 'departure_time'], obj.departureTime);
|
|
8
|
-
cy.GLO_addInput([formName, '
|
|
8
|
+
cy.GLO_addInput([formName, 'departure_timezone'], obj.departureTimezone);
|
|
9
9
|
cy.GLO_addInput([formName, 'arrival_time'], obj.arrivalTime);
|
|
10
|
-
cy.GLO_addInput([formName, '
|
|
10
|
+
cy.GLO_addInput([formName, 'arrival_timezone'], obj.arrivalTimezone);
|
|
11
11
|
typeIfPresent('arrival_relative_days', obj.arrivalRelativeDays);
|
|
12
12
|
typeIfPresent('embark_duration', obj.embarkDuration);
|
|
13
13
|
typeIfPresent('disembark_duration', obj.disembarkDuration);
|
|
@@ -32,6 +32,7 @@ export const GLO_enterCountry = (obj, part = 'country') => {
|
|
|
32
32
|
cy.GLO_checkbox([formName, 'checkbox_use_states'], obj.useStates);
|
|
33
33
|
cy.GLO_addInput([formName, 'street_format'], obj.streetFormat);
|
|
34
34
|
cy.GLO_addInput([formName, 'textbox_addres_block_format'], obj.addressBlockFormat);
|
|
35
|
+
cy.GLO_addInput([formName, 'timezone'], obj.timezone);
|
|
35
36
|
cy.GLO_selectTab([formName, 'tabs_70'], 'Translations', open);
|
|
36
37
|
if (obj.translation !== undefined) {
|
|
37
38
|
cy.GLO_getElement(['ng$basetabletranslation_uuid_lst_tab', 'grid']).within(() => {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export const GLO_enterPostalcode = (obj) => {
|
|
2
2
|
const formName = 'ng$postalcode_dtl';
|
|
3
|
-
|
|
3
|
+
if (obj.country) {
|
|
4
|
+
cy.GLO_addInput([formName, 'country_id'], obj.country);
|
|
5
|
+
// The country's server-side change sets the time zone; wait for it so it cannot overwrite a typed one
|
|
6
|
+
cy.GLO_checkLoadingIndicator();
|
|
7
|
+
}
|
|
4
8
|
cy.GLO_addInput([formName, 'city'], obj.city);
|
|
5
9
|
cy.GLO_addInput([formName, 'postalcode'], obj.PC);
|
|
10
|
+
// After the country, see above
|
|
11
|
+
cy.GLO_addInput([formName, 'timezone'], obj.timezone);
|
|
6
12
|
};
|
|
@@ -70,7 +70,7 @@ Cypress.Commands.add('FOB_oktaLogin', (company, formBuilderStandalone) => {
|
|
|
70
70
|
// Change the Url if necessary
|
|
71
71
|
const url = Cypress.expose('formBuilderUrl');
|
|
72
72
|
cy.visit(url);
|
|
73
|
-
// biome-ignore plugin: wait for okta redirect page load
|
|
73
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for okta redirect page load
|
|
74
74
|
cy.wait(2000);
|
|
75
75
|
cy.task('log', 'Redirect to okta url');
|
|
76
76
|
cy.origin(Cypress.expose('oktaUrl'), () => {
|
|
@@ -133,7 +133,7 @@ Cypress.Commands.add('FOB_deleteCustomViewItem', (name) => {
|
|
|
133
133
|
});
|
|
134
134
|
Cypress.Commands.add('FOB_dragComponentItem', (dragLocation, dropLocation) => {
|
|
135
135
|
cy.get(`[title="${dragLocation}"]`).realMouseDown({ button: 'left', position: 'center' }).realMouseMove(0, 10, { position: 'center' });
|
|
136
|
-
// biome-ignore plugin: wait for drag animation to settle before drop
|
|
136
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for drag animation to settle before drop
|
|
137
137
|
cy.wait(200);
|
|
138
138
|
cy.get(span).contains(dropLocation).realMouseMove(0, 0, { position: 'center' }).realMouseUp();
|
|
139
139
|
cy.GLO_checkLoadingIndicator();
|
|
@@ -119,7 +119,7 @@ Cypress.Commands.add('GLO_defaultlogin', (company, usernameENV = 'username', pas
|
|
|
119
119
|
cy.GLO_checkLoadingIndicator();
|
|
120
120
|
cy.get('body').then(($body) => {
|
|
121
121
|
if ($body.find('.svy-message-dialog.active .window-title').text() === 'Locked record') {
|
|
122
|
-
// biome-ignore plugin: wait for locked record dialog choice to register
|
|
122
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for locked record dialog choice to register
|
|
123
123
|
cy.wait(15000);
|
|
124
124
|
cy.GLO_dialogChoice('!globis.label.yes!');
|
|
125
125
|
}
|
|
@@ -223,7 +223,7 @@ Cypress.Commands.add('GLO_oktaLogin', (company, urlType, skipvNavCheck) => {
|
|
|
223
223
|
}
|
|
224
224
|
else {
|
|
225
225
|
// This is in case the check is instable
|
|
226
|
-
// biome-ignore plugin: wait in case the check is instable
|
|
226
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait in case the check is instable
|
|
227
227
|
cy.wait(5000);
|
|
228
228
|
cy.GLO_checkLoadingIndicator();
|
|
229
229
|
}
|
|
@@ -401,7 +401,7 @@ const waitForProgressBar = () => {
|
|
|
401
401
|
reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 1000)}s waiting for the progress bar to disappear`));
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
|
-
// biome-ignore plugin: wait for next poll interval before checking again
|
|
404
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
|
|
405
405
|
cy.wait(pollInterval, { log: false }).then(() => {
|
|
406
406
|
log.set({ message: `Still waiting for the progress bar ${attempts}/${maxAttempts}` });
|
|
407
407
|
checkStatus(resolve, reject);
|
|
@@ -418,7 +418,7 @@ Cypress.Commands.add('GLO_checkLoadingIndicator', () => {
|
|
|
418
418
|
cy.WS_loadingCheck();
|
|
419
419
|
}
|
|
420
420
|
else {
|
|
421
|
-
// biome-ignore plugin: wait before checking loading indicator
|
|
421
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait before checking loading indicator
|
|
422
422
|
cy.wait(1000);
|
|
423
423
|
cy.get('loading-indicator', { timeout: loadingCheckTimeout }).should('not.exist');
|
|
424
424
|
waitForProgressBar();
|
|
@@ -723,16 +723,22 @@ Cypress.Commands.add('GLO_closeDashboard', () => {
|
|
|
723
723
|
Cypress.Commands.add('GLO_selectTab', (selector, tabName, openOrClose) => {
|
|
724
724
|
cy.GLO_getElement(selector)
|
|
725
725
|
.contains('[tabcontainer] .tab', tabName)
|
|
726
|
-
|
|
726
|
+
// The tab header is a div, so Cypress's `:disabled` actionability check doesn't apply: the click goes through
|
|
727
|
+
// and the server silently ignores it. Closing a disabled tab that is open fails too, because the close request
|
|
728
|
+
// goes through the same click on the same disabled tab, which the server ignores the same way. A tab can also
|
|
729
|
+
// be disabled only transiently (e.g. right after selecting a parent record), so this retries like any other
|
|
730
|
+
// Cypress assertion instead of failing on the first check.
|
|
731
|
+
.should(($tab) => {
|
|
727
732
|
const isOpenAttr = $tab[0].getAttribute('open'); // Attribute is a string 'true' or missing -> null
|
|
728
733
|
const isOpen = isOpenAttr === 'true'; // Convert to boolean
|
|
734
|
+
if (openOrClose !== isOpen && $tab[0].hasAttribute('disabled')) {
|
|
735
|
+
throw new Error(`Tab "${tabName}" in ${selector.join('.')} is disabled and cannot be ${openOrClose ? 'opened' : 'closed'}`);
|
|
736
|
+
}
|
|
737
|
+
})
|
|
738
|
+
.then(($tab) => {
|
|
739
|
+
const isOpenAttr = $tab[0].getAttribute('open');
|
|
740
|
+
const isOpen = isOpenAttr === 'true';
|
|
729
741
|
if (openOrClose !== isOpen) {
|
|
730
|
-
// The tab header is a div, so Cypress's `:disabled` actionability check doesn't apply: the click goes through
|
|
731
|
-
// and the server silently ignores it. Closing a disabled tab that is open fails too, because the close request
|
|
732
|
-
// goes through the same click on the same disabled tab, which the server ignores the same way.
|
|
733
|
-
if ($tab[0].hasAttribute('disabled')) {
|
|
734
|
-
throw new Error(`Tab "${tabName}" in ${selector.join('.')} is disabled and cannot be ${openOrClose ? 'opened' : 'closed'}`);
|
|
735
|
-
}
|
|
736
742
|
const action = openOrClose ? 'Opening' : 'Closing';
|
|
737
743
|
cy.log(`${action} tab ${tabName}`);
|
|
738
744
|
cy.wrap($tab).click();
|
|
@@ -804,7 +810,7 @@ Cypress.Commands.add('GLO_closeProgram', (topBottom, programName, tabNumber) =>
|
|
|
804
810
|
cy.GLO_log(` - Closed program tab ${programName}`);
|
|
805
811
|
});
|
|
806
812
|
Cypress.Commands.add('GLO_dialogChoice', (confirmation) => {
|
|
807
|
-
// biome-ignore plugin: wait for dialog choice body to render
|
|
813
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for dialog choice body to render
|
|
808
814
|
cy.wait(500);
|
|
809
815
|
cy.get('body').then(($body) => {
|
|
810
816
|
let CSSSearchStr;
|
|
@@ -994,21 +1000,44 @@ Cypress.Commands.add('GLO_filterGrid', (colId, selector, text, exactMatch = noEq
|
|
|
994
1000
|
}
|
|
995
1001
|
});
|
|
996
1002
|
if (filterType) {
|
|
1003
|
+
// The typed value needs to sync into the grid's filter model before the operator
|
|
1004
|
+
// menu opens, or the menu's own value field still reads empty and the operator
|
|
1005
|
+
// picker starts from a stale/default state (reproduced manually: opening the menu
|
|
1006
|
+
// too soon after typing shows the filter value field blank).
|
|
1007
|
+
cy.GLO_checkLoadingIndicator();
|
|
997
1008
|
cy.get('@filterHeaderCell').find('button').click();
|
|
998
|
-
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1009
|
+
// Selecting the operator in this popup is occasionally flaky in Cypress (the click
|
|
1010
|
+
// registers but the picker display doesn't update to the new operator - reproduced
|
|
1011
|
+
// manually in the live app and ruled out as a wrong-popup/stale-aria-controls issue;
|
|
1012
|
+
// the open+select sequence itself is correct). Self-heal by reopening and reselecting
|
|
1013
|
+
// instead of guessing at the exact cause of the dropped click.
|
|
1014
|
+
// A compound AND/OR filter always has a second condition row scaffolded in (even
|
|
1015
|
+
// when unused), each with its own operator picker - scope to .first() throughout so
|
|
1016
|
+
// this always targets the primary condition's picker instead of matching 2 elements.
|
|
1017
|
+
const selectFilterType = (attemptsLeft) => {
|
|
1018
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().click();
|
|
1019
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: special dropdown for filter, doesn't work without small wait
|
|
1020
|
+
cy.wait(250);
|
|
1021
|
+
cy.get('.ag-filter-menu .ag-picker-expanded')
|
|
1022
|
+
.invoke('attr', 'aria-controls')
|
|
1023
|
+
.then((dropDownId) => {
|
|
1024
|
+
cy.get(`[id="${dropDownId}"]`)
|
|
1025
|
+
.should('be.visible')
|
|
1026
|
+
.within(() => {
|
|
1027
|
+
cy.get('.ag-list-item').contains(filterType).should('be.visible').parent().click();
|
|
1028
|
+
});
|
|
1008
1029
|
});
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1030
|
+
cy.GLO_checkAGLoader();
|
|
1031
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display')
|
|
1032
|
+
.first()
|
|
1033
|
+
.then(($el) => {
|
|
1034
|
+
if ($el.text().trim() !== filterType && attemptsLeft > 0) {
|
|
1035
|
+
selectFilterType(attemptsLeft - 1);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
selectFilterType(2);
|
|
1040
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().should('contain.text', filterType);
|
|
1012
1041
|
cy.press(Cypress.Keyboard.Keys.ESC); // Close the filter menu
|
|
1013
1042
|
}
|
|
1014
1043
|
cy.GLO_checkLoadingIndicator();
|
|
@@ -1057,13 +1086,13 @@ const selectYear = (targetYear) => {
|
|
|
1057
1086
|
const currentYear = Number(currentDate.split(' ')[1]);
|
|
1058
1087
|
if (currentYear < targetYear) {
|
|
1059
1088
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1060
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1089
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1061
1090
|
cy.wait(500);
|
|
1062
1091
|
selectYear(targetYear);
|
|
1063
1092
|
}
|
|
1064
1093
|
else if (currentYear > targetYear) {
|
|
1065
1094
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1066
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1095
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1067
1096
|
cy.wait(500);
|
|
1068
1097
|
selectYear(targetYear);
|
|
1069
1098
|
}
|
|
@@ -1083,13 +1112,13 @@ const selectMonth = (targetMonth) => {
|
|
|
1083
1112
|
throw new Error('Given month is invalid');
|
|
1084
1113
|
if (currenMonthNumber < targetMonth) {
|
|
1085
1114
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1086
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1115
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1087
1116
|
cy.wait(500);
|
|
1088
1117
|
selectMonth(targetMonth);
|
|
1089
1118
|
}
|
|
1090
1119
|
else if (currenMonthNumber > targetMonth) {
|
|
1091
1120
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1092
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1121
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1093
1122
|
cy.wait(500);
|
|
1094
1123
|
selectMonth(targetMonth);
|
|
1095
1124
|
}
|
|
@@ -1452,8 +1481,8 @@ Cypress.Commands.add('GLO_validateCheckbox', (selector, value) => {
|
|
|
1452
1481
|
* It ensures that the AG Grid has finished loading.
|
|
1453
1482
|
*/
|
|
1454
1483
|
Cypress.Commands.add('GLO_checkAGLoader', () => {
|
|
1455
|
-
// biome-ignore plugin: wait because AGLoader is not instant
|
|
1456
|
-
cy.wait(1000);
|
|
1484
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait because AGLoader is not instant
|
|
1485
|
+
cy.wait(1000);
|
|
1457
1486
|
cy.get('.ag-loading', { timeout: 600000 }).should('not.exist');
|
|
1458
1487
|
});
|
|
1459
1488
|
Cypress.Commands.add('GLO_log', (logValue) => {
|
|
@@ -1629,7 +1658,7 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1629
1658
|
reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s: row count was ${actual}, expected "${lengthCondition} ${numberToValidate}"`));
|
|
1630
1659
|
return;
|
|
1631
1660
|
}
|
|
1632
|
-
// biome-ignore plugin: wait for next poll interval before checking again
|
|
1661
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
|
|
1633
1662
|
cy.wait(pollInterval, { log: false }).then(() => {
|
|
1634
1663
|
log.set({ message: `Still polling ${actual} to ${lengthCondition} ${numberToValidate} ${attempts}/${maxAttempts}` });
|
|
1635
1664
|
checkStatus(resolve, reject);
|
|
@@ -1825,7 +1854,8 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1825
1854
|
.some((cell) => cell.textContent?.trim() === rowFilter))
|
|
1826
1855
|
.find(`[col-id="${columnId}"]`)
|
|
1827
1856
|
.click();
|
|
1828
|
-
|
|
1857
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1858
|
+
cy.wait(500);
|
|
1829
1859
|
cy.GLO_getAllRowsInGrid()
|
|
1830
1860
|
.filter((_, el) => Cypress.$(el)
|
|
1831
1861
|
.find('[role="gridcell"]')
|
|
@@ -1838,7 +1868,8 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1838
1868
|
cy.GLO_getElement(selector).within(() => {
|
|
1839
1869
|
cy.GLO_clickRowInGrid(rowFilter, columnId);
|
|
1840
1870
|
});
|
|
1841
|
-
|
|
1871
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1872
|
+
cy.wait(500);
|
|
1842
1873
|
cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
|
|
1843
1874
|
}
|
|
1844
1875
|
cy.get(gridInputType === 'aggrid-texteditor-without-enter' ? gridInput : gridInputType, { timeout: 1000 }).should('exist');
|
|
@@ -1935,8 +1966,8 @@ Cypress.Commands.add('GLO_clickButtonInGrid', { prevSubject: ['optional', 'eleme
|
|
|
1935
1966
|
});
|
|
1936
1967
|
Cypress.Commands.add('GLO_oktaAlternativeLogin', (altUser, company) => {
|
|
1937
1968
|
cy.GLO_logout();
|
|
1938
|
-
// biome-ignore plugin: wait for Okta to process logout
|
|
1939
|
-
cy.wait(10000);
|
|
1969
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for Okta to process logout
|
|
1970
|
+
cy.wait(10000);
|
|
1940
1971
|
if (cypressWebsocketLoader) {
|
|
1941
1972
|
cy.on('window:before:load', (win) => {
|
|
1942
1973
|
attachWebSocketSpy(win);
|
|
@@ -2263,9 +2294,18 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2263
2294
|
if (value === 'Select All')
|
|
2264
2295
|
return;
|
|
2265
2296
|
filterComponent()
|
|
2266
|
-
.find(
|
|
2267
|
-
.
|
|
2268
|
-
|
|
2297
|
+
.find(getFilterFieldSelector(path, dropDownType, clauseType, false))
|
|
2298
|
+
.then(($field) => {
|
|
2299
|
+
// A tag box adds a tag, a select box (single valuelist) shows the value in its input.
|
|
2300
|
+
// A tag box is a select box too (it carries both classes), so test for the tag box.
|
|
2301
|
+
if ($field.hasClass('dx-tagbox'))
|
|
2302
|
+
cy.wrap($field)
|
|
2303
|
+
.find('.dx-tag')
|
|
2304
|
+
.filter((_, el) => (el.getAttribute('aria-label') ?? '').includes(value))
|
|
2305
|
+
.should('have.length.greaterThan', 0);
|
|
2306
|
+
else
|
|
2307
|
+
cy.wrap($field).find('.dx-texteditor-input').should('have.value', value);
|
|
2308
|
+
});
|
|
2269
2309
|
break;
|
|
2270
2310
|
case 'field':
|
|
2271
2311
|
case 'logical-operator':
|
|
@@ -2345,13 +2385,25 @@ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
|
|
|
2345
2385
|
});
|
|
2346
2386
|
return;
|
|
2347
2387
|
case '=':
|
|
2348
|
-
// biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional - falls through to the shared date/time handling below when v isn't check/uncheck
|
|
2349
2388
|
case '≠':
|
|
2350
2389
|
if (v === check || v === unCheck) {
|
|
2351
2390
|
checkBox(path, v);
|
|
2352
2391
|
return;
|
|
2353
2392
|
}
|
|
2354
|
-
|
|
2393
|
+
// A single-valuelist field is a select box, which ignores a typed value on enter. Typing searches
|
|
2394
|
+
// its list, which renders only a page of items, so the value is typed first and then picked.
|
|
2395
|
+
filterComponent()
|
|
2396
|
+
.find(getFilterFieldSelector(path, 'value', undefined, false))
|
|
2397
|
+
.then(($field) => {
|
|
2398
|
+
const input = () => cy.wrap($field).find('.dx-texteditor-input');
|
|
2399
|
+
if ($field.hasClass('dx-selectbox')) {
|
|
2400
|
+
input().type(`{selectall}{del}${v}`).should('have.attr', 'aria-controls');
|
|
2401
|
+
dropDown(path, clauseType, 'value', String(v));
|
|
2402
|
+
}
|
|
2403
|
+
else
|
|
2404
|
+
input().type(`{selectall}{del}${v}{enter}`);
|
|
2405
|
+
});
|
|
2406
|
+
return;
|
|
2355
2407
|
case 'before':
|
|
2356
2408
|
case 'after':
|
|
2357
2409
|
case 'contains':
|
|
@@ -12,7 +12,7 @@ Cypress.Commands.add('POR_checkPortalSideMenuCorrect', (menuItems) => {
|
|
|
12
12
|
});
|
|
13
13
|
Cypress.Commands.add('POR_portalLogin', () => {
|
|
14
14
|
cy.visit(Cypress.expose('portalUrl'));
|
|
15
|
-
// biome-ignore plugin: wait for portal login redirect
|
|
15
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for portal login redirect
|
|
16
16
|
cy.wait(2000);
|
|
17
17
|
cy.origin(Cypress.expose('oktaUrl'), () => {
|
|
18
18
|
cy.get('.o-form-input')
|
|
@@ -492,7 +492,7 @@ Cypress.Commands.add('WS_loadingCheck', (options) => {
|
|
|
492
492
|
}
|
|
493
493
|
win.lockedRecordRetries = retries + 1;
|
|
494
494
|
debugLog(`🔍 Locked record dialog detected (attempt ${win.lockedRecordRetries}/3), waiting 60s before handling`);
|
|
495
|
-
// biome-ignore plugin: <internal> Cypress wait for locked record dialog handling
|
|
495
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: <internal> Cypress wait for locked record dialog handling
|
|
496
496
|
cy.wait(LOCKED_RECORD_WAIT_MS).then(() => {
|
|
497
497
|
if (!lockedRecordDialogHandled) {
|
|
498
498
|
lockedRecordDialogHandled = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globisnv/cypress-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Core building blocks for Globis Cypress E2E tests",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
|
-
"
|
|
36
|
+
"biome-plugins",
|
|
37
37
|
"bin",
|
|
38
38
|
"template",
|
|
39
39
|
"biome.json"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"uuid": "^14.0.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@biomejs/biome": "^2.5.
|
|
60
|
+
"@biomejs/biome": "^2.5.15",
|
|
61
61
|
"@types/node": "^26.1.2",
|
|
62
62
|
"cypress": "^16.1.0",
|
|
63
63
|
"cypress-real-events": "^1.15.1",
|
package/template/biome.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.5.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
|
|
3
3
|
"extends": ["./node_modules/@globisnv/cypress-core/biome.json"],
|
|
4
|
+
"plugins": ["./node_modules/@globisnv/cypress-core/biome-plugins/noUnnecessaryWaiting.grit"],
|
|
4
5
|
"files": {
|
|
5
6
|
"includes": ["!cypress/screenshots", "!cypress/videos", "!cypress/downloads", "!cypress/reports", "!cypress.env*.json"]
|
|
6
7
|
}
|
package/template/package.json
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"typescript": "^7.0.2"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.5.
|
|
12
|
+
"@biomejs/biome": "^2.5.15",
|
|
13
13
|
"@types/node": "^26.1.2",
|
|
14
14
|
"cypress": "^16.0.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"lint": "biome check .
|
|
18
|
-
"lint:fix": "biome check . --write
|
|
17
|
+
"lint": "biome check .",
|
|
18
|
+
"lint:fix": "biome check . --write",
|
|
19
19
|
"typescript": "tsc --noEmit --skipLibCheck",
|
|
20
20
|
"open": "cypress open --e2e --browser chrome",
|
|
21
21
|
"record": "node ./cypress/support/record.mjs"
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Standalone replacement for the biome-plugins/no-unnecessary-waiting.grit plugin.
|
|
3
|
-
//
|
|
4
|
-
// Biome's GritQL plugins (2.5.7) have no suppression mechanism - a
|
|
5
|
-
// register_diagnostic() call either fires or it doesn't, with no per-line
|
|
6
|
-
// ignore comment support (confirmed: `biome lint --suppress` writes nothing
|
|
7
|
-
// for plugin-sourced diagnostics). Blocking on cy.wait(<number>) while still
|
|
8
|
-
// allowing a documented exception needs code that can check for a preceding
|
|
9
|
-
// comment itself, so this is a plain AST walk instead of a Grit plugin.
|
|
10
|
-
//
|
|
11
|
-
// Flags cy.wait(<number literal>[, options]) unless the line directly above
|
|
12
|
-
// the call has a `// biome-ignore plugin: <reason>` comment.
|
|
13
|
-
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import * as ts from 'typescript/unstable/ast';
|
|
16
|
-
import { API } from 'typescript/unstable/sync';
|
|
17
|
-
|
|
18
|
-
const ROOT = process.cwd(); // invoking repo's root, not this script's install location
|
|
19
|
-
const TSCONFIG = 'tsconfig.json';
|
|
20
|
-
const IGNORE_COMMENT = /^\s*\/\/\s*biome-ignore\s+plugin:/;
|
|
21
|
-
|
|
22
|
-
function walkAll(node, visit) {
|
|
23
|
-
visit(node);
|
|
24
|
-
node.forEachChild((child) => walkAll(child, visit));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isIgnored(sourceFile, node) {
|
|
28
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
29
|
-
if (line === 0) return false;
|
|
30
|
-
const lineStarts = sourceFile.getLineStarts();
|
|
31
|
-
const prevLineText = sourceFile.text.slice(lineStarts[line - 1], lineStarts[line]);
|
|
32
|
-
return IGNORE_COMMENT.test(prevLineText);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// A cy.wait(x) call is just as arbitrary when x is a const/default-param alias for a
|
|
36
|
-
// numeric literal (e.g. `const serverTimeout = 5000`) as when it's inlined. Resolve by
|
|
37
|
-
// walking up to the nearest enclosing block/function and looking for a declaration of
|
|
38
|
-
// that name among its statements/parameters - not a file-wide name search, since two
|
|
39
|
-
// unrelated functions can each declare their own same-named local (e.g. `pollInterval`).
|
|
40
|
-
function resolveNumericAlias(identifier) {
|
|
41
|
-
const name = identifier.text;
|
|
42
|
-
for (let scope = identifier.parent; scope; scope = scope.parent) {
|
|
43
|
-
if (ts.isFunctionLikeDeclaration(scope)) {
|
|
44
|
-
for (const param of scope.parameters) {
|
|
45
|
-
if (ts.isIdentifier(param.name) && param.name.text === name && param.initializer && ts.isNumericLiteral(param.initializer)) {
|
|
46
|
-
return param.initializer.text;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (ts.isBlock(scope) || ts.isSourceFile(scope)) {
|
|
51
|
-
const statements = scope.statements;
|
|
52
|
-
for (const statement of statements) {
|
|
53
|
-
if (!ts.isVariableStatement(statement)) continue;
|
|
54
|
-
for (const decl of statement.declarationList.declarations) {
|
|
55
|
-
if (ts.isIdentifier(decl.name) && decl.name.text === name && decl.initializer && ts.isNumericLiteral(decl.initializer)) {
|
|
56
|
-
return decl.initializer.text;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function checkSourceFile(sourceFile) {
|
|
66
|
-
const violations = [];
|
|
67
|
-
walkAll(sourceFile, (node) => {
|
|
68
|
-
if (!ts.isCallExpression(node)) return;
|
|
69
|
-
const callee = node.expression;
|
|
70
|
-
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression) || callee.expression.text !== 'cy') return;
|
|
71
|
-
if (callee.name.text !== 'wait') return;
|
|
72
|
-
|
|
73
|
-
const msArg = node.arguments[0];
|
|
74
|
-
if (!msArg) return;
|
|
75
|
-
const isDirectLiteral = ts.isNumericLiteral(msArg);
|
|
76
|
-
const isNumericAlias = ts.isIdentifier(msArg) && resolveNumericAlias(msArg) !== undefined;
|
|
77
|
-
if (!isDirectLiteral && !isNumericAlias) return;
|
|
78
|
-
if (isIgnored(sourceFile, node)) return;
|
|
79
|
-
|
|
80
|
-
violations.push({
|
|
81
|
-
file: sourceFile.fileName,
|
|
82
|
-
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
return violations;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function main() {
|
|
89
|
-
const api = new API({ cwd: ROOT });
|
|
90
|
-
try {
|
|
91
|
-
const snapshot = api.updateSnapshot({ openProjects: [TSCONFIG] });
|
|
92
|
-
const project = snapshot.getProject(TSCONFIG);
|
|
93
|
-
|
|
94
|
-
const cypressRoot = path.join(ROOT, 'cypress') + path.sep;
|
|
95
|
-
const fileNames = project.rootFiles.filter((f) => f.startsWith(cypressRoot) && f.endsWith('.ts') && !f.endsWith('.d.ts'));
|
|
96
|
-
|
|
97
|
-
const allViolations = [];
|
|
98
|
-
for (const fileName of fileNames) {
|
|
99
|
-
const sourceFile = project.program.getSourceFile(fileName);
|
|
100
|
-
if (!sourceFile) continue;
|
|
101
|
-
allViolations.push(...checkSourceFile(sourceFile));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const v of allViolations) {
|
|
105
|
-
console.log(
|
|
106
|
-
`${path.relative(ROOT, v.file)}:${v.line} Avoid arbitrary cy.wait(ms); prefer cy.intercept()/should() polling instead. (add "// biome-ignore plugin: <reason>" above the line if intentional)`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (allViolations.length > 0) {
|
|
111
|
-
console.log(`\n${allViolations.length} unguarded cy.wait(ms) call(s) found.`);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
} finally {
|
|
115
|
-
api.close();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
main();
|