@globisnv/cypress-core 1.1.2 → 1.1.4
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_commandlist.js +1 -1
- package/dist/blueprints/GLO_consignment.d.ts +2 -0
- package/dist/blueprints/GLO_consignment.js +13 -4
- package/dist/blueprints/GLO_quotationLine.js +5 -0
- package/dist/commands/FOB_commands.js +2 -2
- package/dist/commands/GLO_commands.js +28 -31
- package/dist/commands/GLS_commands.js +7 -23
- 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
|
|
|
@@ -164,7 +164,7 @@ src/
|
|
|
164
164
|
├── commands/ # 6 generic command files (~128 custom commands)
|
|
165
165
|
├── apiMethods/ # IGS REST request/response helpers
|
|
166
166
|
└── plugins/ # prometheusWriter — SigV4-signed remote write to AWS AMP
|
|
167
|
-
|
|
167
|
+
biome-plugins/ # GritQL lint rules (shipped to consumers)
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
### Command families
|
|
@@ -241,23 +241,22 @@ npm run lint # biome check .
|
|
|
241
241
|
npm run lint:fix # biome check . --write
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
Formatting is Biome ([biome.json](biome.json)): 4-space indent, single quotes, semicolons, no trailing commas, 180-column lines, LF endings.
|
|
244
|
+
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.
|
|
245
245
|
|
|
246
|
-
### Shipped lint
|
|
246
|
+
### Shipped lint plugins
|
|
247
247
|
|
|
248
|
-
`
|
|
248
|
+
`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.
|
|
249
249
|
|
|
250
|
-
|
|
|
250
|
+
| Plugin | What it does |
|
|
251
251
|
| --- | --- |
|
|
252
|
-
| [
|
|
252
|
+
| [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. |
|
|
253
253
|
|
|
254
|
-
Wire it into the consumer repo
|
|
254
|
+
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`:
|
|
255
255
|
|
|
256
256
|
```json
|
|
257
257
|
{
|
|
258
|
-
"
|
|
259
|
-
|
|
260
|
-
}
|
|
258
|
+
"extends": ["./node_modules/@globisnv/cypress-core/biome.json"],
|
|
259
|
+
"plugins": ["./node_modules/@globisnv/cypress-core/biome-plugins/noUnnecessaryWaiting.grit"]
|
|
261
260
|
}
|
|
262
261
|
```
|
|
263
262
|
|
|
@@ -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.14/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
|
}
|
|
@@ -5,6 +5,6 @@ export const GLO_enterCommandlist = (obj) => {
|
|
|
5
5
|
cy.GLO_addDate(input, [formName, 'valid_from'], obj.validFrom);
|
|
6
6
|
cy.GLO_addDate(input, [formName, 'valid_till'], obj.validTo);
|
|
7
7
|
cy.GLO_checkbox([formName, 'allow_automatic_update'], obj.allowAutomaticUpdate);
|
|
8
|
-
cy.GLO_popupWindow([formName, '
|
|
8
|
+
cy.GLO_popupWindow([formName, 'relation_id'], obj.customerRelation);
|
|
9
9
|
cy.GLO_addInput([formName, 'commandlist_code'], obj.commandlistCode);
|
|
10
10
|
};
|
|
@@ -56,6 +56,8 @@ export type objConsignment = {
|
|
|
56
56
|
etd?: string;
|
|
57
57
|
eta?: string;
|
|
58
58
|
fileReference?: string;
|
|
59
|
+
loadStartRequested?: string;
|
|
60
|
+
unloadStartRequested?: string;
|
|
59
61
|
};
|
|
60
62
|
export declare const GLO_enterConsignment: (obj: objConsignment, formName?: string, formNameGeneral?: string, formNameSea?: string, formNameWarehouse?: string) => void;
|
|
61
63
|
export declare const GLO_enterConsignment$Terminal: (obj: objConsignment) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { close, equal, input, open } from '../tsHelper.js';
|
|
1
|
+
import { close, equal, equalBool, input, open, typeSearch } from '../tsHelper.js';
|
|
2
2
|
import { GLO_enterAddressPopup } from './GLO_addressPopup.js';
|
|
3
3
|
const defaultFormName = 'ng$consignment_dtl';
|
|
4
4
|
const defaultFormNameGeneral = 'ng$consignment_dtl_general';
|
|
@@ -25,7 +25,14 @@ export const GLO_enterConsignment = (obj, formName = defaultFormName, formNameGe
|
|
|
25
25
|
cy.GLO_validateCheckbox([formName, 'flag_spot'], obj.flagSpotOrder);
|
|
26
26
|
cy.GLO_addInput([formName, 'plan_bloc'], obj.planBloc);
|
|
27
27
|
cy.GLO_addInput([formName, 'file_reference_txt'], obj.fileReference);
|
|
28
|
-
if (obj.collectionAddress ||
|
|
28
|
+
if (obj.collectionAddress ||
|
|
29
|
+
obj.deliveryAddress ||
|
|
30
|
+
obj.loadStartBooked ||
|
|
31
|
+
obj.loadEndBooked ||
|
|
32
|
+
obj.loadStartBookedTime ||
|
|
33
|
+
obj.loadEndBookedTime ||
|
|
34
|
+
obj.loadStartRequested ||
|
|
35
|
+
obj.unloadStartRequested) {
|
|
29
36
|
cy.GLO_selectTab([formName, 'tabs_70'], 'General', open);
|
|
30
37
|
if (obj.collectionAddress) {
|
|
31
38
|
cy.GLO_clickButton([formNameGeneral, 'btn_select_from_address'], 'fa-search-plus');
|
|
@@ -39,6 +46,8 @@ export const GLO_enterConsignment = (obj, formName = defaultFormName, formNameGe
|
|
|
39
46
|
obj.deliveryAddress.type ??= 'search';
|
|
40
47
|
GLO_enterAddressPopup(obj.deliveryAddress);
|
|
41
48
|
}
|
|
49
|
+
cy.GLO_addDate(input, [formNameGeneral, 'load_start_requested'], obj.loadStartRequested);
|
|
50
|
+
cy.GLO_addDate(input, [formNameGeneral, 'unload_start_requested'], obj.unloadStartRequested);
|
|
42
51
|
cy.GLO_addDate(input, [formNameGeneral, 'load_start_booked'], obj.loadStartBooked);
|
|
43
52
|
cy.GLO_addDate(input, [formNameGeneral, 'load_start_booked_time'], obj.loadStartBookedTime);
|
|
44
53
|
cy.GLO_addDate(input, [formNameGeneral, 'load_end_booked'], obj.loadEndBooked);
|
|
@@ -83,8 +92,8 @@ const seaTab = (obj, formName, formNameSea) => {
|
|
|
83
92
|
cy.GLO_addInput([formNameSea, 'txt_shipper_reference'], obj.shipperRef);
|
|
84
93
|
cy.GLO_addInput([formNameSea, 'supplier_incoterm'], obj.shipperIncoTerm);
|
|
85
94
|
cy.GLO_addInput([formNameSea, 'quay_code'], obj.quayCode);
|
|
86
|
-
cy.
|
|
87
|
-
cy.
|
|
95
|
+
cy.GLO_popupWindow([formNameSea, 'from_logistic_location'], obj.POL, undefined, equalBool, typeSearch);
|
|
96
|
+
cy.GLO_popupWindow([formNameSea, 'to_logistic_location'], obj.POD, undefined, equalBool, typeSearch);
|
|
88
97
|
cy.GLO_addInput([formNameSea, 'transport_method'], obj.transportMethod);
|
|
89
98
|
cy.GLO_addDate(input, [formNameSea, 'atd'], obj.atd);
|
|
90
99
|
cy.GLO_addDate(input, [formNameSea, 'goods_closing'], obj.goodsClosing);
|
|
@@ -11,6 +11,11 @@ const enterQuotationLine = (useNew, obj) => {
|
|
|
11
11
|
const formNameInnerBreak = 'ng$quotationlines_dtl_quantity';
|
|
12
12
|
const articleSelector = useNew ? 'article_code' : 'btn_select_article';
|
|
13
13
|
cy.GLO_popupWindow([formName, articleSelector], obj.articleCode);
|
|
14
|
+
// The article selection triggers a double load, so wait until the field really holds the article
|
|
15
|
+
// A falsy article code leaves the current article untouched, so there is nothing to wait for
|
|
16
|
+
if (obj.articleCode) {
|
|
17
|
+
cy.GLO_checkField([formName, 'article_code'], obj.articleCode, undefined, 300000);
|
|
18
|
+
}
|
|
14
19
|
if (obj.quantityBreaks && obj.quantityBreaks.length > 0) {
|
|
15
20
|
cy.GLO_selectTab([formName, 'tabs_470'], 'Quantity breaks', open);
|
|
16
21
|
obj.quantityBreaks.forEach((qtyBreak, index) => {
|
|
@@ -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();
|
|
@@ -116,7 +116,7 @@ Cypress.Commands.add('GLO_defaultlogin', (company, usernameENV = 'username', pas
|
|
|
116
116
|
cy.GLO_checkLoadingIndicator();
|
|
117
117
|
cy.get('body').then(($body) => {
|
|
118
118
|
if ($body.find('.svy-message-dialog.active .window-title').text() === 'Locked record') {
|
|
119
|
-
// biome-ignore plugin: wait for locked record dialog choice to register
|
|
119
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for locked record dialog choice to register
|
|
120
120
|
cy.wait(15000);
|
|
121
121
|
cy.GLO_dialogChoice('!globis.label.yes!');
|
|
122
122
|
}
|
|
@@ -220,7 +220,7 @@ Cypress.Commands.add('GLO_oktaLogin', (company, urlType, skipvNavCheck) => {
|
|
|
220
220
|
}
|
|
221
221
|
else {
|
|
222
222
|
// This is in case the check is instable
|
|
223
|
-
// biome-ignore plugin: wait in case the check is instable
|
|
223
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait in case the check is instable
|
|
224
224
|
cy.wait(5000);
|
|
225
225
|
cy.GLO_checkLoadingIndicator();
|
|
226
226
|
}
|
|
@@ -398,7 +398,7 @@ const waitForProgressBar = () => {
|
|
|
398
398
|
reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 1000)}s waiting for the progress bar to disappear`));
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
401
|
-
// biome-ignore plugin: wait for next poll interval before checking again
|
|
401
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
|
|
402
402
|
cy.wait(pollInterval, { log: false }).then(() => {
|
|
403
403
|
log.set({ message: `Still waiting for the progress bar ${attempts}/${maxAttempts}` });
|
|
404
404
|
checkStatus(resolve, reject);
|
|
@@ -415,7 +415,7 @@ Cypress.Commands.add('GLO_checkLoadingIndicator', () => {
|
|
|
415
415
|
cy.WS_loadingCheck();
|
|
416
416
|
}
|
|
417
417
|
else {
|
|
418
|
-
// biome-ignore plugin: wait before checking loading indicator
|
|
418
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait before checking loading indicator
|
|
419
419
|
cy.wait(1000);
|
|
420
420
|
cy.get('loading-indicator', { timeout: loadingCheckTimeout }).should('not.exist');
|
|
421
421
|
waitForProgressBar();
|
|
@@ -776,7 +776,7 @@ Cypress.Commands.add('GLO_closeProgram', (topBottom, programName, tabNumber) =>
|
|
|
776
776
|
cy.GLO_log(` - Closed program tab ${programName}`);
|
|
777
777
|
});
|
|
778
778
|
Cypress.Commands.add('GLO_dialogChoice', (confirmation) => {
|
|
779
|
-
// biome-ignore plugin: wait for dialog choice body to render
|
|
779
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for dialog choice body to render
|
|
780
780
|
cy.wait(500);
|
|
781
781
|
cy.get('body').then(($body) => {
|
|
782
782
|
let CSSSearchStr;
|
|
@@ -982,7 +982,8 @@ Cypress.Commands.add('GLO_filterGrid', (colId, selector, text, exactMatch = noEq
|
|
|
982
982
|
// this always targets the primary condition's picker instead of matching 2 elements.
|
|
983
983
|
const selectFilterType = (attemptsLeft) => {
|
|
984
984
|
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().click();
|
|
985
|
-
|
|
985
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: special dropdown for filter, doesn't work without small wait
|
|
986
|
+
cy.wait(250);
|
|
986
987
|
cy.get('.ag-filter-menu .ag-picker-expanded')
|
|
987
988
|
.invoke('attr', 'aria-controls')
|
|
988
989
|
.then((dropDownId) => {
|
|
@@ -1051,13 +1052,13 @@ const selectYear = (targetYear) => {
|
|
|
1051
1052
|
const currentYear = Number(currentDate.split(' ')[1]);
|
|
1052
1053
|
if (currentYear < targetYear) {
|
|
1053
1054
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1054
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1055
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1055
1056
|
cy.wait(500);
|
|
1056
1057
|
selectYear(targetYear);
|
|
1057
1058
|
}
|
|
1058
1059
|
else if (currentYear > targetYear) {
|
|
1059
1060
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1060
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1061
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1061
1062
|
cy.wait(500);
|
|
1062
1063
|
selectYear(targetYear);
|
|
1063
1064
|
}
|
|
@@ -1077,13 +1078,13 @@ const selectMonth = (targetMonth) => {
|
|
|
1077
1078
|
throw new Error('Given month is invalid');
|
|
1078
1079
|
if (currenMonthNumber < targetMonth) {
|
|
1079
1080
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1080
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1081
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1081
1082
|
cy.wait(500);
|
|
1082
1083
|
selectMonth(targetMonth);
|
|
1083
1084
|
}
|
|
1084
1085
|
else if (currenMonthNumber > targetMonth) {
|
|
1085
1086
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1086
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1087
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1087
1088
|
cy.wait(500);
|
|
1088
1089
|
selectMonth(targetMonth);
|
|
1089
1090
|
}
|
|
@@ -1335,7 +1336,8 @@ Cypress.Commands.add('GLO_popupWindow', (selector, value, columnId, equals = noE
|
|
|
1335
1336
|
selectFromPopupWindow(value, columnId, equals);
|
|
1336
1337
|
}
|
|
1337
1338
|
else {
|
|
1338
|
-
|
|
1339
|
+
// Type search with an exact match must stay exact: a contains check would accept LOC1 for LOC10.
|
|
1340
|
+
cy.GLO_checkField(selector, value, equals ? (useMagnifierFlag ? contains : equal) : undefined);
|
|
1339
1341
|
}
|
|
1340
1342
|
});
|
|
1341
1343
|
});
|
|
@@ -1451,8 +1453,8 @@ Cypress.Commands.add('GLO_validateCheckbox', (selector, value) => {
|
|
|
1451
1453
|
* It ensures that the AG Grid has finished loading.
|
|
1452
1454
|
*/
|
|
1453
1455
|
Cypress.Commands.add('GLO_checkAGLoader', () => {
|
|
1454
|
-
// biome-ignore plugin: wait because AGLoader is not instant
|
|
1455
|
-
cy.wait(1000);
|
|
1456
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait because AGLoader is not instant
|
|
1457
|
+
cy.wait(1000);
|
|
1456
1458
|
cy.get('.ag-loading', { timeout: 600000 }).should('not.exist');
|
|
1457
1459
|
});
|
|
1458
1460
|
Cypress.Commands.add('GLO_log', (logValue) => {
|
|
@@ -1626,7 +1628,7 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1626
1628
|
reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s: row count was ${actual}, expected "${lengthCondition} ${numberToValidate}"`));
|
|
1627
1629
|
return;
|
|
1628
1630
|
}
|
|
1629
|
-
// biome-ignore plugin: wait for next poll interval before checking again
|
|
1631
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
|
|
1630
1632
|
cy.wait(pollInterval, { log: false }).then(() => {
|
|
1631
1633
|
log.set({ message: `Still polling ${actual} to ${lengthCondition} ${numberToValidate} ${attempts}/${maxAttempts}` });
|
|
1632
1634
|
checkStatus(resolve, reject);
|
|
@@ -1822,7 +1824,8 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1822
1824
|
.some((cell) => cell.textContent?.trim() === rowFilter))
|
|
1823
1825
|
.find(`[col-id="${columnId}"]`)
|
|
1824
1826
|
.click();
|
|
1825
|
-
|
|
1827
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1828
|
+
cy.wait(500);
|
|
1826
1829
|
cy.GLO_getAllRowsInGrid()
|
|
1827
1830
|
.filter((_, el) => Cypress.$(el)
|
|
1828
1831
|
.find('[role="gridcell"]')
|
|
@@ -1838,7 +1841,8 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1838
1841
|
// Re-check outside the .within(): loading-indicator renders at page level, so the check
|
|
1839
1842
|
// inside GLO_clickRowInGrid (scoped to the grid element above) can't see it and resolves early.
|
|
1840
1843
|
cy.GLO_checkLoadingIndicator();
|
|
1841
|
-
|
|
1844
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1845
|
+
cy.wait(500);
|
|
1842
1846
|
cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
|
|
1843
1847
|
}
|
|
1844
1848
|
cy.get(gridInputType === 'aggrid-texteditor-without-enter' ? gridInput : gridInputType, { timeout: 1000 }).should('exist');
|
|
@@ -1935,8 +1939,8 @@ Cypress.Commands.add('GLO_clickButtonInGrid', { prevSubject: ['optional', 'eleme
|
|
|
1935
1939
|
});
|
|
1936
1940
|
Cypress.Commands.add('GLO_oktaAlternativeLogin', (altUser, company) => {
|
|
1937
1941
|
cy.GLO_logout();
|
|
1938
|
-
// biome-ignore plugin: wait for Okta to process logout
|
|
1939
|
-
cy.wait(10000);
|
|
1942
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for Okta to process logout
|
|
1943
|
+
cy.wait(10000);
|
|
1940
1944
|
if (cypressWebsocketLoader) {
|
|
1941
1945
|
cy.on('window:before:load', (win) => {
|
|
1942
1946
|
attachWebSocketSpy(win);
|
|
@@ -2102,21 +2106,14 @@ const validateGridValue = (value, failValue, rowFilter, columnId, timeOut = 1000
|
|
|
2102
2106
|
cy.then(() => promise);
|
|
2103
2107
|
}
|
|
2104
2108
|
};
|
|
2105
|
-
Cypress.Commands.
|
|
2106
|
-
return
|
|
2107
|
-
|
|
2108
|
-
.find('#txtCount')
|
|
2109
|
-
.invoke('text')
|
|
2110
|
-
.then((records) => {
|
|
2111
|
-
const [currentStr, totalStr] = records.split('/');
|
|
2112
|
-
if (!currentStr || !totalStr) {
|
|
2113
|
-
throw new Error(`Could not parse record count from "${records}"`);
|
|
2114
|
-
}
|
|
2109
|
+
Cypress.Commands.addQuery('GLO_getCurrentRecordCount', () => {
|
|
2110
|
+
return () => {
|
|
2111
|
+
const records = Cypress.$('div[formslist]:not([hidden]) [data-cy$="ng$datacontrol.datacontrol"] #txtCount').text().split('/');
|
|
2115
2112
|
return {
|
|
2116
|
-
currentRecord: parseInt(
|
|
2117
|
-
totalRecords: parseInt(
|
|
2113
|
+
currentRecord: Number.parseInt(records[0]?.trim() ?? '', 10),
|
|
2114
|
+
totalRecords: Number.parseInt(records[1]?.trim() ?? '', 10)
|
|
2118
2115
|
};
|
|
2119
|
-
}
|
|
2116
|
+
};
|
|
2120
2117
|
});
|
|
2121
2118
|
Cypress.Commands.add('GLO_storeObject', (formName, elementNames, index, log = hideInCommandLog) => {
|
|
2122
2119
|
const keys = (Array.isArray(elementNames) ? elementNames : [elementNames]);
|
|
@@ -14,29 +14,13 @@ Cypress.Commands.add('GLS_login', (username = 'PR001', company) => {
|
|
|
14
14
|
cy.GLO_checkLoadingIndicator();
|
|
15
15
|
});
|
|
16
16
|
Cypress.Commands.add('GLS_mainNavigation', (value) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.then(($viewport) => {
|
|
25
|
-
const $label = $viewport.find('label').filter((_i, el) => el.innerText.trim() === value);
|
|
26
|
-
if ($label.length > 0 && $label.is(':visible')) {
|
|
27
|
-
return cy.contains('.ag-body-viewport label', value).click();
|
|
28
|
-
}
|
|
29
|
-
return cy
|
|
30
|
-
.get('.ag-center-cols-container [role="row"][tabindex="0"]')
|
|
31
|
-
.first()
|
|
32
|
-
.focus()
|
|
33
|
-
.realPress('ArrowDown')
|
|
34
|
-
.then(() => scrollAndSearch(attempts + 1));
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
scrollAndSearch().then(() => {
|
|
38
|
-
cy.GLO_checkLoadingIndicator();
|
|
39
|
-
});
|
|
17
|
+
// Exact match, so 'Wegzetten' does not open 'Wegzetten drager'. A menu level that is still mounted but hidden is skipped
|
|
18
|
+
// through its [hidden] container instead of :visible, which also drops items scrolled out of the list (see isInOpenContainer)
|
|
19
|
+
const exactText = new RegExp(`^\\s*${Cypress._.escapeRegExp(value)}\\s*$`);
|
|
20
|
+
cy.get('.svy-listformcomponent label.clickable')
|
|
21
|
+
.filter((_i, el) => !el.closest('[hidden]') && exactText.test(el.innerText))
|
|
22
|
+
.click();
|
|
23
|
+
cy.GLO_checkLoadingIndicator();
|
|
40
24
|
});
|
|
41
25
|
Cypress.Commands.add('GLS_selectStock', (formName, elementName, index) => {
|
|
42
26
|
cy.GLO_getElement([formName, elementName]).find(`.item-container#${index}`).click();
|
|
@@ -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": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Core building blocks for Globis Cypress E2E tests",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
-
"
|
|
37
|
+
"biome-plugins",
|
|
38
38
|
"bin",
|
|
39
39
|
"template",
|
|
40
40
|
"biome.json"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"uuid": "^14.0.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@biomejs/biome": "^2.5.
|
|
61
|
+
"@biomejs/biome": "^2.5.14",
|
|
62
62
|
"@types/node": "^26.1.2",
|
|
63
63
|
"cypress": "^16.1.1",
|
|
64
64
|
"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.14/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.14",
|
|
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();
|