@globisnv/cypress-core 1.1.1 → 1.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_priceRequestLine.js +2 -0
- package/dist/commands/FOB_commands.js +2 -2
- package/dist/commands/GLO_commands.js +63 -42
- package/dist/commands/POR_commands.js +1 -1
- package/dist/websocketDetector.js +1 -1
- package/package.json +6 -5
- 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
|
}
|
|
@@ -12,6 +12,8 @@ export const GLO_enterPriceRequestLine$Terminal = (obj) => {
|
|
|
12
12
|
cy.GLO_addInput([qtFormName, 'gross_price'], obj.grossPriceTwo);
|
|
13
13
|
cy.GLO_addInput([qtFormName, 'purchase_costs_euro'], obj.purchaseCostTwo);
|
|
14
14
|
cy.GLO_addInput([qtFormName, 'discount_amount'], obj.discountTwoValue);
|
|
15
|
+
if (obj.discountTwoValue)
|
|
16
|
+
cy.GLO_checkLoadingIndicator();
|
|
15
17
|
cy.GLO_addInput([qtFormName, 'discount_percentage'], obj.discountTwoPercentage);
|
|
16
18
|
});
|
|
17
19
|
};
|
|
@@ -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;
|
|
@@ -966,21 +966,44 @@ Cypress.Commands.add('GLO_filterGrid', (colId, selector, text, exactMatch = noEq
|
|
|
966
966
|
}
|
|
967
967
|
});
|
|
968
968
|
if (filterType) {
|
|
969
|
+
// The typed value needs to sync into the grid's filter model before the operator
|
|
970
|
+
// menu opens, or the menu's own value field still reads empty and the operator
|
|
971
|
+
// picker starts from a stale/default state (reproduced manually: opening the menu
|
|
972
|
+
// too soon after typing shows the filter value field blank).
|
|
973
|
+
cy.GLO_checkLoadingIndicator();
|
|
969
974
|
cy.get('@filterHeaderCell').find('button').click();
|
|
970
|
-
|
|
971
|
-
//
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
975
|
+
// Selecting the operator in this popup is occasionally flaky in Cypress (the click
|
|
976
|
+
// registers but the picker display doesn't update to the new operator - reproduced
|
|
977
|
+
// manually in the live app and ruled out as a wrong-popup/stale-aria-controls issue;
|
|
978
|
+
// the open+select sequence itself is correct). Self-heal by reopening and reselecting
|
|
979
|
+
// instead of guessing at the exact cause of the dropped click.
|
|
980
|
+
// A compound AND/OR filter always has a second condition row scaffolded in (even
|
|
981
|
+
// when unused), each with its own operator picker - scope to .first() throughout so
|
|
982
|
+
// this always targets the primary condition's picker instead of matching 2 elements.
|
|
983
|
+
const selectFilterType = (attemptsLeft) => {
|
|
984
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().click();
|
|
985
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: special dropdown for filter, doesn't work without small wait
|
|
986
|
+
cy.wait(250);
|
|
987
|
+
cy.get('.ag-filter-menu .ag-picker-expanded')
|
|
988
|
+
.invoke('attr', 'aria-controls')
|
|
989
|
+
.then((dropDownId) => {
|
|
990
|
+
cy.get(`[id="${dropDownId}"]`)
|
|
991
|
+
.should('be.visible')
|
|
992
|
+
.within(() => {
|
|
993
|
+
cy.get('.ag-list-item').contains(filterType).should('be.visible').parent().click();
|
|
994
|
+
});
|
|
980
995
|
});
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
996
|
+
cy.GLO_checkAGLoader();
|
|
997
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display')
|
|
998
|
+
.first()
|
|
999
|
+
.then(($el) => {
|
|
1000
|
+
if ($el.text().trim() !== filterType && attemptsLeft > 0) {
|
|
1001
|
+
selectFilterType(attemptsLeft - 1);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
selectFilterType(2);
|
|
1006
|
+
cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').first().should('contain.text', filterType);
|
|
984
1007
|
cy.press(Cypress.Keyboard.Keys.ESC); // Close the filter menu
|
|
985
1008
|
}
|
|
986
1009
|
cy.GLO_checkLoadingIndicator();
|
|
@@ -1029,13 +1052,13 @@ const selectYear = (targetYear) => {
|
|
|
1029
1052
|
const currentYear = Number(currentDate.split(' ')[1]);
|
|
1030
1053
|
if (currentYear < targetYear) {
|
|
1031
1054
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1032
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1055
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1033
1056
|
cy.wait(500);
|
|
1034
1057
|
selectYear(targetYear);
|
|
1035
1058
|
}
|
|
1036
1059
|
else if (currentYear > targetYear) {
|
|
1037
1060
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1038
|
-
// biome-ignore plugin: wait for year navigation click to register
|
|
1061
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for year navigation click to register
|
|
1039
1062
|
cy.wait(500);
|
|
1040
1063
|
selectYear(targetYear);
|
|
1041
1064
|
}
|
|
@@ -1055,13 +1078,13 @@ const selectMonth = (targetMonth) => {
|
|
|
1055
1078
|
throw new Error('Given month is invalid');
|
|
1056
1079
|
if (currenMonthNumber < targetMonth) {
|
|
1057
1080
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
|
|
1058
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1081
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1059
1082
|
cy.wait(500);
|
|
1060
1083
|
selectMonth(targetMonth);
|
|
1061
1084
|
}
|
|
1062
1085
|
else if (currenMonthNumber > targetMonth) {
|
|
1063
1086
|
cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
|
|
1064
|
-
// biome-ignore plugin: wait for month navigation click to register
|
|
1087
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for month navigation click to register
|
|
1065
1088
|
cy.wait(500);
|
|
1066
1089
|
selectMonth(targetMonth);
|
|
1067
1090
|
}
|
|
@@ -1429,8 +1452,8 @@ Cypress.Commands.add('GLO_validateCheckbox', (selector, value) => {
|
|
|
1429
1452
|
* It ensures that the AG Grid has finished loading.
|
|
1430
1453
|
*/
|
|
1431
1454
|
Cypress.Commands.add('GLO_checkAGLoader', () => {
|
|
1432
|
-
// biome-ignore plugin: wait because AGLoader is not instant
|
|
1433
|
-
cy.wait(1000);
|
|
1455
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait because AGLoader is not instant
|
|
1456
|
+
cy.wait(1000);
|
|
1434
1457
|
cy.get('.ag-loading', { timeout: 600000 }).should('not.exist');
|
|
1435
1458
|
});
|
|
1436
1459
|
Cypress.Commands.add('GLO_log', (logValue) => {
|
|
@@ -1604,7 +1627,7 @@ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate,
|
|
|
1604
1627
|
reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s: row count was ${actual}, expected "${lengthCondition} ${numberToValidate}"`));
|
|
1605
1628
|
return;
|
|
1606
1629
|
}
|
|
1607
|
-
// biome-ignore plugin: wait for next poll interval before checking again
|
|
1630
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for next poll interval before checking again
|
|
1608
1631
|
cy.wait(pollInterval, { log: false }).then(() => {
|
|
1609
1632
|
log.set({ message: `Still polling ${actual} to ${lengthCondition} ${numberToValidate} ${attempts}/${maxAttempts}` });
|
|
1610
1633
|
checkStatus(resolve, reject);
|
|
@@ -1800,7 +1823,8 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1800
1823
|
.some((cell) => cell.textContent?.trim() === rowFilter))
|
|
1801
1824
|
.find(`[col-id="${columnId}"]`)
|
|
1802
1825
|
.click();
|
|
1803
|
-
|
|
1826
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1827
|
+
cy.wait(500);
|
|
1804
1828
|
cy.GLO_getAllRowsInGrid()
|
|
1805
1829
|
.filter((_, el) => Cypress.$(el)
|
|
1806
1830
|
.find('[role="gridcell"]')
|
|
@@ -1813,7 +1837,11 @@ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, va
|
|
|
1813
1837
|
cy.GLO_getElement(selector).within(() => {
|
|
1814
1838
|
cy.GLO_clickRowInGrid(rowFilter, columnId);
|
|
1815
1839
|
});
|
|
1816
|
-
|
|
1840
|
+
// Re-check outside the .within(): loading-indicator renders at page level, so the check
|
|
1841
|
+
// inside GLO_clickRowInGrid (scoped to the grid element above) can't see it and resolves early.
|
|
1842
|
+
cy.GLO_checkLoadingIndicator();
|
|
1843
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: else both click and dblclick fire too fast
|
|
1844
|
+
cy.wait(500);
|
|
1817
1845
|
cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
|
|
1818
1846
|
}
|
|
1819
1847
|
cy.get(gridInputType === 'aggrid-texteditor-without-enter' ? gridInput : gridInputType, { timeout: 1000 }).should('exist');
|
|
@@ -1910,8 +1938,8 @@ Cypress.Commands.add('GLO_clickButtonInGrid', { prevSubject: ['optional', 'eleme
|
|
|
1910
1938
|
});
|
|
1911
1939
|
Cypress.Commands.add('GLO_oktaAlternativeLogin', (altUser, company) => {
|
|
1912
1940
|
cy.GLO_logout();
|
|
1913
|
-
// biome-ignore plugin: wait for Okta to process logout
|
|
1914
|
-
cy.wait(10000);
|
|
1941
|
+
// biome-ignore lint/plugin/noUnnecessaryWaiting: wait for Okta to process logout
|
|
1942
|
+
cy.wait(10000);
|
|
1915
1943
|
if (cypressWebsocketLoader) {
|
|
1916
1944
|
cy.on('window:before:load', (win) => {
|
|
1917
1945
|
attachWebSocketSpy(win);
|
|
@@ -2077,21 +2105,14 @@ const validateGridValue = (value, failValue, rowFilter, columnId, timeOut = 1000
|
|
|
2077
2105
|
cy.then(() => promise);
|
|
2078
2106
|
}
|
|
2079
2107
|
};
|
|
2080
|
-
Cypress.Commands.
|
|
2081
|
-
return
|
|
2082
|
-
|
|
2083
|
-
.find('#txtCount')
|
|
2084
|
-
.invoke('text')
|
|
2085
|
-
.then((records) => {
|
|
2086
|
-
const [currentStr, totalStr] = records.split('/');
|
|
2087
|
-
if (!currentStr || !totalStr) {
|
|
2088
|
-
throw new Error(`Could not parse record count from "${records}"`);
|
|
2089
|
-
}
|
|
2108
|
+
Cypress.Commands.addQuery('GLO_getCurrentRecordCount', () => {
|
|
2109
|
+
return () => {
|
|
2110
|
+
const records = Cypress.$('div[formslist]:not([hidden]) [data-cy$="ng$datacontrol.datacontrol"] #txtCount').text().split('/');
|
|
2090
2111
|
return {
|
|
2091
|
-
currentRecord: parseInt(
|
|
2092
|
-
totalRecords: parseInt(
|
|
2112
|
+
currentRecord: Number.parseInt(records[0]?.trim() ?? '', 10),
|
|
2113
|
+
totalRecords: Number.parseInt(records[1]?.trim() ?? '', 10)
|
|
2093
2114
|
};
|
|
2094
|
-
}
|
|
2115
|
+
};
|
|
2095
2116
|
});
|
|
2096
2117
|
Cypress.Commands.add('GLO_storeObject', (formName, elementNames, index, log = hideInCommandLog) => {
|
|
2097
2118
|
const keys = (Array.isArray(elementNames) ? elementNames : [elementNames]);
|
|
@@ -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.3",
|
|
4
4
|
"description": "Core building blocks for Globis Cypress E2E tests",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"url": "git+https://github.com/globisnv/globis-cypress-core.git"
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
15
|
-
"access": "public"
|
|
15
|
+
"access": "public",
|
|
16
|
+
"tag": "release-1.x"
|
|
16
17
|
},
|
|
17
18
|
"globisOnline": {
|
|
18
19
|
"minVersion": "11.0.2"
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"dist",
|
|
36
|
-
"
|
|
37
|
+
"biome-plugins",
|
|
37
38
|
"bin",
|
|
38
39
|
"template",
|
|
39
40
|
"biome.json"
|
|
@@ -57,9 +58,9 @@
|
|
|
57
58
|
"uuid": "^14.0.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@biomejs/biome": "^2.5.
|
|
61
|
+
"@biomejs/biome": "^2.5.14",
|
|
61
62
|
"@types/node": "^26.1.2",
|
|
62
|
-
"cypress": "^16.1.
|
|
63
|
+
"cypress": "^16.1.1",
|
|
63
64
|
"cypress-real-events": "^1.15.1",
|
|
64
65
|
"typescript": "^7.0.2"
|
|
65
66
|
},
|
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();
|